Attribution modelling

case_study1.jpg

"Let’s show a bird’s-eye view of our digital channel performance from this year first. Here we could have a bar chart on the number of online purchases and some advertising data on mobile, paid search, affiliations, and social media activities. And some correlations here as well. What do you think?" said Levi.

"Perhaps Prisha will ask about the overall improvement compared to last year, right? So, let’s add the same figures from last year and make a comparison here too," responded Emma.

Levi and Emma, the marketing science team members at KNC – a European firm that sells trendy and quality make-up and skincare products at affordable prices for students and young professionals – sat round a modern-look, high-gloss conference table to discuss what to include in their presentation deck prior to their meeting with Prisha, the chief marketing officer (CMO) of KNC.

"I am sure that during the meeting, she will ask about each channel’s contribution to final purchases. This is quite critical for next year’s budget request. How about drilling down to the individual-level data and running some attribution models?" said Levi.

Emma nodded in agreement and said, “We can show the results from the ‘first-touch’ and ‘last-touch’ heuristics, and let her decide which model to use for budget-related decisions. Most brands use the ‘last-touch’ model, anyway.”

After a long day at work, Levi and Emma agreed on what to cover in their presentation for the CMO: an overview of digital channel performance and some detailed results from attribution modelling.

Meeting with the CMO

case_study2.jpg

"Well, these were the results from the first-touch attribution model. Now, the results from the last-touch model [Figure 4.1]. This model gives full credit to the last digital channel that consumers interacted with right before making a purchase. According to this attribution model, the least effective digital channel is “paid search”. It contributes a mere 12 per cent to conversions. The most effective is mobile. It contributes 39 per cent. Affiliations surprisingly account for 27 per cent in sales conversions. Social media has a moderate impact: its contribution is 22 per cent. Therefore, we propose to reduce our efforts for paid search campaigns and increase our investments in mobile," said Emma.

case_study4.jpg

Prisha interrupted the presentation and said, "I am a little bit puzzled here, Emma. It is hard to believe that the social media channel has a lower impact on consumer purchases than affiliates. Most of our targeted consumers are young adults who spend considerable time on social networks. These results are not convincing enough."

Pointing to the title below of an online article from her laptop screen (Hercher, 2021), Prisha continued: ‘Look at this.’

case_study3.jpg

"Google won’t use the last-click attribution as the default conversion model. I am not sure what exactly they mean by 'data modelling', but they seem to be right in ditching the last-touch model. It is very clear that assigning full credit for a sale to the last digital channel in the consumer’s path to purchase may mislead us."

Meanwhile, as a recently graduated marketing analyst, Levi opens his laptop, digs up his analytics course content and rediscovers that there are other approaches to attribution modelling.

Inspired by his class materials, Levi responded to Prisha: "Well, we can use more advanced models – for example, Markov chain models – to get a better picture of attribution across digital channels. Also, we can use the Shapley value-based modelling approach to quantify the effect of a channel removal. What would happen if we turn off a channel? Perhaps by turning off a particular channel we would not lose much on conversion rates…"

"Excellent suggestions," said Prisha. "My gut feeling is telling me that the model should assign different weights to all the touchpoints on the customer journey. Why don’t you start right now? I will discuss our digital marketing budget with the board next week. So, we need to be quick on this."

The Python code below will help us address the following questions on the KNC case study:

  • Taking a path-to-purchase view, which customer touchpoint[^In this Python application, the words 'channels' and 'touchpoints' are used interchangeably] contributes more or less to conversion?

  • Should KNC give up on paid search and put more effort into mobile marketing?

  • Which attribution modelling approach should the KNC marketing analysts, Levi and Emma, adopt in the future?

Preparation and set-up¶

Before we roll out the analyses, we need to ensure that all our files are organised, and our Python environment is set up. To do so, we follow the steps below:

  • Set the working directory.

  • Install and load the Python libraries needed for this application.

  • Download the dataset to our working directory.

In [60]:
# The following code sets a seed for random number generation. 
# It ensures that the sequence of random numbers generated by functions from the 'random' module remains consistent across different runs of the code. 

# Set the seed
import random
random.seed(1810)

Create a folder and set a working directory¶

We create a folder on our computer and name it 'chapter 4'. Then, we run the following code to set the working directory in Python.

In [61]:
import os
cwd = os.getcwd()
print(cwd)

#Change the working directory 
os.chdir('C:/Users/gyildiri/Downloads/chapter 4')
print(os.getcwd())
C:\Users\gyildiri\Downloads\chapter 4
C:\Users\gyildiri\Downloads\chapter 4

The first part of the above code retrieves the current working directory, and the second part changes the working directory to the folder we have just created. Ensure that you update the path to reflect your specific directory. Next, we install the Python libraries. If you encounter any issues while installing the following libraries, go to 'Microsoft C++ Build Tools' ( https://visualstudio.microsoft.com/visual-cpp-build-tools/ ). Download and install the 'Build Tools for Visual Studio'. During the installation, make sure to select the C++ build tools and the SDK version appropriate for your operating system.

In [9]:
# Install required Python packages if not already installed
%pip install pandas
%pip install openpyxl
%pip install matplotlib
%pip install seaborn
%pip install scikit-learn
%pip install markovchain
%pip install pymarkovchain
%pip install ChannelAttribution
Requirement already satisfied: pandas in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (2.1.4)
Requirement already satisfied: numpy<2,>=1.23.2 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from pandas) (1.26.4)
Requirement already satisfied: python-dateutil>=2.8.2 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from pandas) (2.8.2)
Requirement already satisfied: pytz>=2020.1 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from pandas) (2023.3.post1)
Requirement already satisfied: tzdata>=2022.1 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from pandas) (2023.3)
Requirement already satisfied: six>=1.5 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from python-dateutil>=2.8.2->pandas) (1.16.0)
Note: you may need to restart the kernel to use updated packages.
Requirement already satisfied: openpyxl in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (3.0.10)
Requirement already satisfied: et_xmlfile in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from openpyxl) (1.1.0)
Note: you may need to restart the kernel to use updated packages.
Requirement already satisfied: matplotlib in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (3.8.0)Note: you may need to restart the kernel to use updated packages.

Requirement already satisfied: contourpy>=1.0.1 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from matplotlib) (1.2.0)
Requirement already satisfied: cycler>=0.10 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from matplotlib) (0.11.0)
Requirement already satisfied: fonttools>=4.22.0 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from matplotlib) (4.25.0)
Requirement already satisfied: kiwisolver>=1.0.1 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from matplotlib) (1.4.4)
Requirement already satisfied: numpy<2,>=1.21 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from matplotlib) (1.26.4)
Requirement already satisfied: packaging>=20.0 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from matplotlib) (23.1)
Requirement already satisfied: pillow>=6.2.0 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from matplotlib) (10.2.0)
Requirement already satisfied: pyparsing>=2.3.1 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from matplotlib) (3.0.9)
Requirement already satisfied: python-dateutil>=2.7 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from matplotlib) (2.8.2)
Requirement already satisfied: six>=1.5 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from python-dateutil>=2.7->matplotlib) (1.16.0)
Requirement already satisfied: seaborn in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (0.12.2)
Requirement already satisfied: numpy!=1.24.0,>=1.17 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from seaborn) (1.26.4)
Requirement already satisfied: pandas>=0.25 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from seaborn) (2.1.4)
Requirement already satisfied: matplotlib!=3.6.1,>=3.1 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from seaborn) (3.8.0)
Requirement already satisfied: contourpy>=1.0.1 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from matplotlib!=3.6.1,>=3.1->seaborn) (1.2.0)
Requirement already satisfied: cycler>=0.10 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from matplotlib!=3.6.1,>=3.1->seaborn) (0.11.0)
Requirement already satisfied: fonttools>=4.22.0 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from matplotlib!=3.6.1,>=3.1->seaborn) (4.25.0)
Requirement already satisfied: kiwisolver>=1.0.1 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from matplotlib!=3.6.1,>=3.1->seaborn) (1.4.4)
Requirement already satisfied: packaging>=20.0 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from matplotlib!=3.6.1,>=3.1->seaborn) (23.1)
Requirement already satisfied: pillow>=6.2.0 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from matplotlib!=3.6.1,>=3.1->seaborn) (10.2.0)
Requirement already satisfied: pyparsing>=2.3.1 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from matplotlib!=3.6.1,>=3.1->seaborn) (3.0.9)
Requirement already satisfied: python-dateutil>=2.7 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from matplotlib!=3.6.1,>=3.1->seaborn) (2.8.2)
Requirement already satisfied: pytz>=2020.1 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from pandas>=0.25->seaborn) (2023.3.post1)
Requirement already satisfied: tzdata>=2022.1 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from pandas>=0.25->seaborn) (2023.3)
Requirement already satisfied: six>=1.5 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from python-dateutil>=2.7->matplotlib!=3.6.1,>=3.1->seaborn) (1.16.0)
Note: you may need to restart the kernel to use updated packages.
Requirement already satisfied: scikit-learn in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (1.2.2)
Requirement already satisfied: numpy>=1.17.3 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from scikit-learn) (1.26.4)
Requirement already satisfied: scipy>=1.3.2 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from scikit-learn) (1.11.4)
Requirement already satisfied: joblib>=1.1.1 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from scikit-learn) (1.2.0)
Requirement already satisfied: threadpoolctl>=2.0.0 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from scikit-learn) (2.2.0)
Note: you may need to restart the kernel to use updated packages.
Requirement already satisfied: markovchain in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (0.2.6)
Requirement already satisfied: enum34 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from markovchain) (1.1.10)
Requirement already satisfied: tqdm in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from markovchain) (4.65.0)
Requirement already satisfied: custom-inherit in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from markovchain) (2.4.1)
Requirement already satisfied: colorama in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from tqdm->markovchain) (0.4.6)
Note: you may need to restart the kernel to use updated packages.
Requirement already satisfied: pymarkovchain in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (1.8)
Note: you may need to restart the kernel to use updated packages.
Requirement already satisfied: ChannelAttribution in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (2.1.7)
Requirement already satisfied: numpy in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from ChannelAttribution) (1.26.4)
Requirement already satisfied: pandas in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from ChannelAttribution) (2.1.4)
Requirement already satisfied: python-dateutil>=2.8.2 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from pandas->ChannelAttribution) (2.8.2)
Requirement already satisfied: pytz>=2020.1 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from pandas->ChannelAttribution) (2023.3.post1)
Requirement already satisfied: tzdata>=2022.1 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from pandas->ChannelAttribution) (2023.3)
Requirement already satisfied: six>=1.5 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from python-dateutil>=2.8.2->pandas->ChannelAttribution) (1.16.0)
Note: you may need to restart the kernel to use updated packages.
In [62]:
import pandas as pd
import numpy as np
import openpyxl
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn import preprocessing
from pymarkovchain import MarkovChain
from IPython.display import display
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn import metrics
from tabulate import tabulate

Next, we will load the dataset.

Consumer path-to-purchase data¶

The dataset of this case study is available on the website of the book. The file is named 'knc_attribution', and stored in a csv format. We run the following code chunk to load the dataset to Python.

In [63]:
file_path = 'knc_attribution.csv'

# Load the dataset into a pandas DataFrame
attribution = pd.read_csv(file_path)

# Display the first few rows of the DataFrame to check if it loaded correctly
print(attribution.head())
   users_id      channel1      channel2     channel3      channel4  \
0         1        mobile   paid_search  paid_search   paid_search   
1         2   paid_search        mobile       mobile   paid_search   
2         3  social_media  social_media       mobile     affiliate   
3         4        mobile     affiliate       mobile  social_media   
4         5   paid_search   paid_search    affiliate   paid_search   

       channel5     channel6      channel7     channel8      channel9  ...  \
0     affiliate       mobile   paid_search  paid_search  social_media  ...   
1     affiliate  paid_search        mobile  paid_search        mobile  ...   
2  social_media  paid_search        mobile       mobile     affiliate  ...   
3     affiliate       mobile  social_media       mobile           NaN  ...   
4        mobile  paid_search   paid_search    affiliate        mobile  ...   

  channel38 channel39 channel40 channel41 channel42 channel43 channel44  \
0       NaN       NaN       NaN       NaN       NaN       NaN       NaN   
1       NaN       NaN       NaN       NaN       NaN       NaN       NaN   
2       NaN       NaN       NaN       NaN       NaN       NaN       NaN   
3       NaN       NaN       NaN       NaN       NaN       NaN       NaN   
4       NaN       NaN       NaN       NaN       NaN       NaN       NaN   

  channel45 user_purchase null_purchase  
0       NaN             0             1  
1       NaN             1             0  
2       NaN             1             0  
3       NaN             1             0  
4       NaN             1             0  

[5 rows x 48 columns]

Looking at the dataset attribution from the upper right corner of the R Studio screen, we see that we are given 105 unique online consumer journeys, of which 89 turned into conversion. This corresponds to 85% of conversion rate. Note that this conversion rate is quite high. We typically observe such high conversion rates around heavy promotional times.

The below table provides information on the variables of the dataset and their descriptions.

In [64]:
var_desc_data = {
    'Variable': ['users_id', 'channeln', 'user_purchase', 'null_purchase'],
    'Description': [
        'Unique user ID',
        'Online advertising exposure path, with channel1 being the first exposure, channel2 the second, ..., channeln the nth (maximum number of exposures = 45).',
        'Consumer final conversion indicator, 1 if the consumer made a purchase and 0 otherwise.',
        'The opposite of user_purchase, 1 if there is no purchase and 0 otherwise.'
    ]
}

# Create DataFrame
var_desc = pd.DataFrame(var_desc_data)

# Display the DataFrame
print(tabulate(var_desc, headers='keys', tablefmt='pretty'))
+---+---------------+----------------------------------------------------------------------------------------------------------------------------------------------------------+
|   |   Variable    |                                                                       Description                                                                        |
+---+---------------+----------------------------------------------------------------------------------------------------------------------------------------------------------+
| 0 |   users_id    |                                                                      Unique user ID                                                                      |
| 1 |   channeln    | Online advertising exposure path, with channel1 being the first exposure, channel2 the second, ..., channeln the nth (maximum number of exposures = 45). |
| 2 | user_purchase |                                 Consumer final conversion indicator, 1 if the consumer made a purchase and 0 otherwise.                                  |
| 3 | null_purchase |                                        The opposite of user_purchase, 1 if there is no purchase and 0 otherwise.                                         |
+---+---------------+----------------------------------------------------------------------------------------------------------------------------------------------------------+

After getting a glimpse of the data, we can start running the attribution models.

Markov chain approach¶

Data preparation¶

To estimate a Markov chain model, we start by knitting consumer advertising exposures into a desired 'sequenced path' format for the analysis.The below code generates 'conversion sequences' for all consumers.

In [65]:
# Construct conversion sequences for all visitors
attribution.replace({pd.NaT: ' '}, inplace=True)  # Replace NaN values with empty strings

# Convert channel columns to strings and concatenate into a sequenced path
attribution['path'] = attribution.apply(lambda row: '>'.join(map(str, row[1:46])), axis=1)

# Remove leading and trailing whitespaces and non-alphanumeric characters
attribution['cleaned_path'] = attribution['path'].str.replace('\\s.*', '', regex=True).str.replace('\\W$', '', regex=True)
attribution['cleaned_path'] = attribution['cleaned_path'].str.rstrip('>')

# Display the first few rows to check the result
print(attribution[['path', 'cleaned_path']])
                                                                                                                                                                                                                                                                                     path  \
0                                          mobile>paid_search>paid_search>paid_search>affiliate>mobile>paid_search>paid_search>social_media>mobile>social_media>paid_search>social_media>mobile>affiliate>paid_search>paid_search> > > > > > > > > > > > > > > > > > > > > > > > > > > >    
1        paid_search>mobile>mobile>paid_search>affiliate>paid_search>mobile>paid_search>mobile>mobile>paid_search>affiliate>affiliate>affiliate>affiliate>paid_search>affiliate>affiliate>affiliate>affiliate>affiliate>paid_search>affiliate> > > > > > > > > > > > > > > > > > > > > >    
2                                                                                                                     social_media>social_media>mobile>affiliate>social_media>paid_search>mobile>mobile>affiliate> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > >    
3                                                                                                                                     mobile>affiliate>mobile>social_media>affiliate>mobile>social_media>mobile> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > >    
4    paid_search>paid_search>affiliate>paid_search>mobile>paid_search>paid_search>affiliate>mobile>affiliate>paid_search>affiliate>affiliate>affiliate>affiliate>paid_search>mobile>affiliate>paid_search>affiliate>mobile>mobile>paid_search> > > > > > > > > > > > > > > > > > > > > >    
..                                                                                                                                                                                                                                                                                    ...   
100                                                                                                            affiliate>paid_search>affiliate>social_media>social_media>mobile>affiliate>paid_search>paid_search> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > >    
101                                             paid_search>paid_search>social_media>paid_search>mobile>affiliate>paid_search>mobile>paid_search>social_media>affiliate>mobile>mobile>social_media>mobile>paid_search>paid_search> > > > > > > > > > > > > > > > > > > > > > > > > > > >    
102                                                                                                                                   paid_search>social_media>mobile>social_media>social_media>paid_search> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > >    
103                                                                                social_media>paid_search>social_media>mobile>mobile>mobile>paid_search>affiliate>affiliate>paid_search>social_media>mobile>paid_search> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > >    
104                                                                                                               mobile>mobile>paid_search>paid_search>affiliate>mobile>paid_search>paid_search>paid_search>mobile> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > >    

                                                                                                                                                                                                                                 cleaned_path  
0                                                      mobile>paid_search>paid_search>paid_search>affiliate>mobile>paid_search>paid_search>social_media>mobile>social_media>paid_search>social_media>mobile>affiliate>paid_search>paid_search  
1        paid_search>mobile>mobile>paid_search>affiliate>paid_search>mobile>paid_search>mobile>mobile>paid_search>affiliate>affiliate>affiliate>affiliate>paid_search>affiliate>affiliate>affiliate>affiliate>affiliate>paid_search>affiliate  
2                                                                                                                                                 social_media>social_media>mobile>affiliate>social_media>paid_search>mobile>mobile>affiliate  
3                                                                                                                                                                   mobile>affiliate>mobile>social_media>affiliate>mobile>social_media>mobile  
4    paid_search>paid_search>affiliate>paid_search>mobile>paid_search>paid_search>affiliate>mobile>affiliate>paid_search>affiliate>affiliate>affiliate>affiliate>paid_search>mobile>affiliate>paid_search>affiliate>mobile>mobile>paid_search  
..                                                                                                                                                                                                                                        ...  
100                                                                                                                                        affiliate>paid_search>affiliate>social_media>social_media>mobile>affiliate>paid_search>paid_search  
101                                                         paid_search>paid_search>social_media>paid_search>mobile>affiliate>paid_search>mobile>paid_search>social_media>affiliate>mobile>mobile>social_media>mobile>paid_search>paid_search  
102                                                                                                                                                                     paid_search>social_media>mobile>social_media>social_media>paid_search  
103                                                                                                    social_media>paid_search>social_media>mobile>mobile>mobile>paid_search>affiliate>affiliate>paid_search>social_media>mobile>paid_search  
104                                                                                                                                         mobile>mobile>paid_search>paid_search>affiliate>mobile>paid_search>paid_search>paid_search>mobile  

[105 rows x 2 columns]

If we take a look at the dataset again, we see that there is a new variable created, called cleaned_path.

Now let's look at the paths of first 10 consumers in our dataset:

In [66]:
# Set display options for left-aligned text
pd.set_option('display.max_colwidth', None)
pd.set_option('display.unicode.ambiguous_as_wide', True)

# Convert Series to DataFrame
data_view_df = pd.DataFrame(attribution, columns=['cleaned_path'])

# Display the DataFrame with left-aligned text
data_view_df.style.set_properties(**{'text-align': 'left'})
data_view_df.head(10)
Out[66]:
cleaned_path
0 mobile>paid_search>paid_search>paid_search>affiliate>mobile>paid_search>paid_search>social_media>mobile>social_media>paid_search>social_media>mobile>affiliate>paid_search>paid_search
1 paid_search>mobile>mobile>paid_search>affiliate>paid_search>mobile>paid_search>mobile>mobile>paid_search>affiliate>affiliate>affiliate>affiliate>paid_search>affiliate>affiliate>affiliate>affiliate>affiliate>paid_search>affiliate
2 social_media>social_media>mobile>affiliate>social_media>paid_search>mobile>mobile>affiliate
3 mobile>affiliate>mobile>social_media>affiliate>mobile>social_media>mobile
4 paid_search>paid_search>affiliate>paid_search>mobile>paid_search>paid_search>affiliate>mobile>affiliate>paid_search>affiliate>affiliate>affiliate>affiliate>paid_search>mobile>affiliate>paid_search>affiliate>mobile>mobile>paid_search
5 mobile>mobile>social_media>social_media>affiliate>affiliate>paid_search>mobile>social_media>affiliate>affiliate>mobile>paid_search>mobile>social_media>social_media>social_media>social_media>affiliate>paid_search>mobile>affiliate>paid_search>mobile>mobile>mobile>paid_search>affiliate>mobile>mobile>paid_search>affiliate>social_media>mobile>mobile>affiliate>paid_search>affiliate>mobile
6 paid_search>mobile>paid_search>mobile>paid_search>paid_search>social_media>affiliate>paid_search>mobile>mobile>social_media>affiliate>social_media
7 affiliate>paid_search>paid_search>mobile>affiliate>social_media>mobile>paid_search>social_media>mobile>paid_search>mobile>mobile>affiliate>social_media>paid_search>mobile>mobile>affiliate>affiliate>affiliate>mobile>mobile>paid_search>paid_search>paid_search>social_media>mobile>mobile>mobile>mobile
8 affiliate>paid_search>affiliate>paid_search>social_media>mobile>social_media>paid_search>social_media>affiliate>social_media>paid_search>mobile>paid_search>social_media>mobile>paid_search>mobile>mobile>paid_search>affiliate>paid_search>social_media>social_media>social_media>mobile>paid_search>social_media>mobile>affiliate>affiliate>mobile>affiliate>mobile>paid_search>social_media>mobile>mobile>paid_search>affiliate>social_media>mobile>social_media>social_media>mobile
9 affiliate>paid_search>affiliate>paid_search>affiliate>affiliate>affiliate>affiliate>affiliate>mobile>affiliate>paid_search>paid_search>mobile>paid_search>paid_search>affiliate>paid_search>affiliate>paid_search>affiliate>mobile>affiliate>paid_search>affiliate>mobile>affiliate>affiliate>paid_search>paid_search>paid_search>mobile>affiliate>affiliate>affiliate>affiliate>paid_search>affiliate>paid_search>affiliate>affiliate

We can already observe some heterogeneity across consumers in terms of:

  1. starting touchpoint (i.e., first-touch)

  2. ending touchpoint before (non)conversion (i.e., last-touch), and

  3. composition and length of paths.

For model estimation, we decide to take the first 80 consumers in the dataset as our training set and keep the remaining 25 observations for the test set. The below code chunk splits the data into training and test sets.

In [67]:
# Define training and testing sets
attribution_train = attribution.iloc[:80, :]
attribution_test = attribution.iloc[80:, :]

# Display the shapes of the training and test sets
print("Training set shape:", attribution_train.shape)
print("Test set shape:", attribution_test.shape)
Training set shape: (80, 50)
Test set shape: (25, 50)

As a final preparatory step, we need to create a 'channel stack', through which we summarize consumer paths and calculate the total number of conversions (frequency)and non-conversions for each path pattern. The following code chunk will help us generate that channel stack:

In [68]:
# Create a channel stack
channel_stack = attribution_train.groupby('cleaned_path').agg({
    'user_purchase': 'sum',
    'null_purchase': 'sum'
}).reset_index()

channel_stack.columns = ['cleaned_path', 'conversion', 'non_conversion']

# Display the left-aligned channel stack using Styler
channel_stack.head(10).style.set_properties(**{'text-align': 'left'}).set_table_styles([{
    'selector': 'th',
    'props': [('text-align', 'left')]
}])
Out[68]:
  cleaned_path conversion non_conversion
0 affiliate>mobile>affiliate>social_media>social_media>mobile>mobile>mobile>mobile 1 0
1 affiliate>paid_search>affiliate>paid_search>affiliate>affiliate>affiliate>affiliate>affiliate>mobile>affiliate>paid_search>paid_search>mobile>paid_search>paid_search>affiliate>paid_search>affiliate>paid_search>affiliate>mobile>affiliate>paid_search>affiliate>mobile>affiliate>affiliate>paid_search>paid_search>paid_search>mobile>affiliate>affiliate>affiliate>affiliate>paid_search>affiliate>paid_search>affiliate>affiliate 1 0
2 affiliate>paid_search>affiliate>paid_search>mobile>mobile>paid_search>paid_search>mobile>affiliate>affiliate>paid_search>mobile>paid_search>affiliate>mobile>paid_search>mobile>mobile>paid_search>affiliate>paid_search>affiliate>social_media>paid_search>mobile>paid_search>affiliate>mobile>affiliate>affiliate>mobile>affiliate>mobile>paid_search>mobile>mobile>mobile>paid_search>affiliate>affiliate>mobile>affiliate>paid_search>mobile 1 0
3 affiliate>paid_search>affiliate>paid_search>social_media>mobile>social_media>paid_search>social_media>affiliate>social_media>paid_search>mobile>paid_search>social_media>mobile>paid_search>mobile>mobile>paid_search>affiliate>paid_search>social_media>social_media>social_media>mobile>paid_search>social_media>mobile>affiliate>affiliate>mobile>affiliate>mobile>paid_search>social_media>mobile>mobile>paid_search>affiliate>social_media>mobile>social_media>social_media>mobile 4 0
4 affiliate>paid_search>affiliate>social_media>social_media>mobile>mobile>paid_search>mobile 2 2
5 affiliate>paid_search>paid_search>mobile>affiliate>social_media>mobile>paid_search>social_media>mobile>paid_search>mobile>mobile>affiliate>social_media>paid_search>mobile>mobile>affiliate>affiliate>affiliate>mobile>mobile>paid_search>paid_search>paid_search>social_media>mobile>mobile>mobile>mobile 5 0
6 mobile>affiliate>mobile>social_media>affiliate>mobile>social_media>mobile 2 0
7 mobile>mobile>affiliate>paid_search>affiliate>mobile>affiliate>paid_search>paid_search>mobile 1 0
8 mobile>mobile>social_media>social_media>affiliate>affiliate>paid_search>mobile>social_media>affiliate>affiliate>mobile>paid_search>mobile>social_media>social_media>social_media>social_media>affiliate>paid_search>mobile>affiliate>paid_search>mobile>mobile>mobile>paid_search>affiliate>mobile>mobile>paid_search>affiliate>social_media>mobile>mobile>affiliate>paid_search>affiliate>mobile 5 0
9 mobile>paid_search>affiliate>paid_search>mobile>mobile>affiliate>paid_search>paid_search>mobile 0 1

Now we are ready to estimate the Markov chain model with the cleaned version of our data.

Markov chain modelling¶

To estimate the Markov chain model, we use the markov_model function. Specifically, we estimate a third-order Markov model so that the 'memory' of the chain goes back to the most recent three states. Our assumption here is that consumer journeys typically cannot be restricted to the most recent state, i.e., consumers have a longer memory. Recall that first-order Markov model assumes that the current state is only determined by the previous or the most recent state.

In [69]:
import ChannelAttribution

markov=ChannelAttribution.markov_model(Data=channel_stack,var_path='cleaned_path',var_conv='conversion',var_null='non_conversion',order=3)
markov_df=pd.DataFrame({'channel':markov['channel_name'],'total_conversions':round(markov['total_conversions']),'percent':round((markov['total_conversions']/sum(attribution_train['user_purchase'])),3)})

markov_df
Number of simulations: 100000 - Convergence reached: 0.31% < 5.00%
Percentage of simulated paths that successfully end before maximum number of steps (44) is reached: 92.25%
*** Looking to run more advanced attribution? Try ChannelAttribution Pro for free! Visit https://channelattribution.io/product
Out[69]:
channel total_conversions percent
0 affiliate 16.0 0.242
1 mobile 18.0 0.265
2 social_media 16.0 0.234
3 paid_search 17.0 0.259

The results based on Markov chain model tell us quite a different story compared to what the KNC analysts found using the last-touch heuristic. The Markov chain model informs us that:

  • The four touchpoints do not differ dramatically from each other in effectiveness in driving conversions.

  • Both social media advertising and affiliate advertising contribute around 24% to conversion, while both paid search and mobile channels contribute around 26%.

To further make sense of what we have found, we can obtain the transition probabilities between all states through the following codes:

In [70]:
# Trasition matrix of order 3

markov_transition=ChannelAttribution.transition_matrix(Data=channel_stack,var_path='cleaned_path',var_conv='conversion',var_null='non_conversion',order=3)
*** Looking to run more advanced attribution? Try ChannelAttribution Pro for free! Visit https://channelattribution.io/product
In [71]:
markov_transition['channels']
Out[71]:
id_channel channel_name
0 1 affiliate
1 2 mobile
2 3 social_media
3 4 paid_search
In [72]:
markov_transition['transition_matrix']
Out[72]:
channel_from channel_to transition_probability
0 (start) 1 2 1 0.0125
1 (start) 1 4 1 0.1250
2 (start) 1 4 4 0.0625
3 (start) 2 1 2 0.0250
4 (start) 2 2 1 0.0125
... ... ... ...
229 1 3 1 3 1 3 0.5000
230 1 3 1 3 1 1 0.5000
231 1 1 3 1 3 4 1.0000
232 3 4 1 (conversion) 0.8000
233 3 4 1 (null) 0.2000

234 rows × 3 columns

To summarize the transition matrix of third-order Markov model, we obtain the transition probabilities to conversion state using the three-channel paths.

In [73]:
# The sequence of channel names corresponds to
# 1 = affiliate
# 2 = mobile
# 3 = social_media
# 4 = paid_search

# Create a list of channel sequences
channel_sequences = ["3 4 1", "2 3 2", "2 2 2", "2 2 1", "4 1 2", "3 1 3", "3 3 1", "3 3 2", "3 2 1", "2 2 4", "2 4 4", "4 4 2", "2 4 2", "4 2 4"]
# Define a dictionary to map channel numbers to names
channel_mapping = {'1': 'affiliate', '2': 'mobile', '3': 'social_media', '4': 'paid_search'}

# Initialize an empty list to store results
results = []

# Loop through each channel sequence and extract the corresponding transition probability
for sequence in channel_sequences:
    transition_probability = markov_transition['transition_matrix'][
        markov_transition['transition_matrix']['channel_from'] == sequence
    ]['transition_probability'].values[0]
    # Replace channel numbers in the sequence with their names
    channel_names = ' > '.join([channel_mapping[channel] for channel in sequence.split()])

    results.append({'channel_3rd_order': channel_names, 'transition_probability': transition_probability})

# Create a DataFrame from the results
markov_result = pd.DataFrame(results)
#markov_result = markov_result.sort_values(by='transition_probability', ascending=False)


# Print the resulting DataFrame
print(markov_result)
                          channel_3rd_order  transition_probability
0    social_media > paid_search > affiliate                0.800000
1            mobile > social_media > mobile                0.400000
2                  mobile > mobile > mobile                0.272727
3               mobile > mobile > affiliate                0.192308
4          paid_search > affiliate > mobile                0.357143
5   social_media > affiliate > social_media                0.222222
6   social_media > social_media > affiliate                0.416667
7      social_media > social_media > mobile                0.391304
8         social_media > mobile > affiliate                0.153846
9             mobile > mobile > paid_search                0.113208
10       mobile > paid_search > paid_search                0.184211
11       paid_search > paid_search > mobile                0.294118
12            mobile > paid_search > mobile                0.500000
13       paid_search > mobile > paid_search                0.333333

For example, we can see that consumers with the last three channel exposures Social Media > Paid Search > Affiliate has the highest conversion probability (p=0.80).

We can imagine that such a path might highly describe a teenage consumer behaviour online. They are attracted by fancy social network advertising on Instagram or Facebook. Then, they click on paid search advertising to get further information about the product.Later, they get further enticed by repeatedly browsing on affiliate websites, and finally decide to purchase the latest beauty product of KNC.

Question: Considering the Markov transition matrix and path-to-conversion table above, can you try to infer why we have such a different conclusion about social media and affiliate channel contributions based on Markov chain and last-touch modelling approaches?


Visualisation¶

Markov graph

A Markov graph helps us further understand the paths that consumers in our dataset have taken towards conversion and non-conversion. We could plot Markov graph for the third-order Markov chain model that we just estimated. However, due to complexity in visual representation of the third-order model, here we obtain a graph of the first-order Markov chain model to illustrate how to interpret such graphs from Markov chain models.

Running the code chunks below gets us a nice-looking Markov graph.

In [74]:
trans_1st=ChannelAttribution.transition_matrix(Data=channel_stack,var_path='cleaned_path',var_conv='conversion',var_null='non_conversion',order=1)
trans_matrix_1st_o=trans_1st['transition_matrix']
*** Looking to run more advanced attribution? Try ChannelAttribution Pro for free! Visit https://channelattribution.io/product
In [75]:
df_dummy=pd.DataFrame({
    'channel_from':['(start)','(conversion)','(null)'],
    'channel_to':['(start)','(conversion)','(null)'],
    trans_matrix_1st_o.columns.values[2]:[0,1,1]
    })

df_dummy
Out[75]:
channel_from channel_to transition_probability
0 (start) (start) 0
1 (conversion) (conversion) 1
2 (null) (null) 1
In [76]:
trans_matrix_1st_bind=pd.concat([df_dummy,trans_matrix_1st_o],axis=0)
In [27]:
import pandas as pd

# Sample data (replace with your actual DataFrame)
data = {
    'channel_from': trans_matrix_1st_bind['channel_from'],
    'channel_to': trans_matrix_1st_bind['channel_to'],
    'transition_probability': trans_matrix_1st_bind['transition_probability']
}
df = pd.DataFrame(data)

# Pivot the DataFrame
trans_matrix_1st = df.pivot(index='channel_from', columns='channel_to', values='transition_probability')

# Display the pivoted DataFrame
print(trans_matrix_1st)
channel_to    (conversion)    (null)  (start)         1         2         3  \
channel_from                                                                  
(conversion)      1.000000       NaN      NaN       NaN       NaN       NaN   
(null)                 NaN  1.000000      NaN       NaN       NaN       NaN   
(start)                NaN       NaN      0.0  0.200000  0.275000  0.137500   
1                 0.049046  0.008174      NaN  0.212534  0.185286  0.188011   
2                 0.057018  0.010965      NaN  0.197368  0.265351  0.142544   
3                 0.012346  0.003086      NaN  0.237654  0.382716  0.212963   
4                 0.041485  0.008734      NaN  0.231441  0.264192  0.240175   

channel_to           4  
channel_from            
(conversion)       NaN  
(null)             NaN  
(start)       0.387500  
1             0.356948  
2             0.326754  
3             0.151235  
4             0.213974  
In [78]:
import numpy as np

# Convert to numpy matrix, excluding the first column
trans_matrix_final = trans_matrix_1st.iloc[:, 1:].to_numpy()

# If you need row and column names:
row_labels = trans_matrix_1st.iloc[:, 0].tolist()  # First column as row labels
column_labels = trans_matrix_1st.columns[1:].tolist()  # Remaining column names as column labels
In [79]:
# Convert the entire DataFrame to a numpy matrix
trans_matrix_final = trans_matrix_1st.to_numpy()

# For the row labels, we use the index of the DataFrame
row_labels = trans_matrix_1st.index.tolist()

# For the column labels, we use the column names of the DataFrame
column_labels = trans_matrix_1st.columns.tolist()
In [80]:
trans_matrix_final = np.nan_to_num(trans_matrix_final)
In [81]:
trans_matrix_final
Out[81]:
array([[1.        , 0.        , 0.        , 0.        , 0.        ,
        0.        , 0.        ],
       [0.        , 1.        , 0.        , 0.        , 0.        ,
        0.        , 0.        ],
       [0.        , 0.        , 0.        , 0.2       , 0.275     ,
        0.1375    , 0.3875    ],
       [0.04904632, 0.00817439, 0.        , 0.21253406, 0.1852861 ,
        0.1880109 , 0.35694823],
       [0.05701754, 0.01096491, 0.        , 0.19736842, 0.26535088,
        0.14254386, 0.32675439],
       [0.01234568, 0.00308642, 0.        , 0.23765432, 0.38271605,
        0.21296296, 0.15123457],
       [0.04148472, 0.00873362, 0.        , 0.23144105, 0.26419214,
        0.24017467, 0.2139738 ]])
In [82]:
pip install quantecon
Requirement already satisfied: quantecon in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (0.7.2)
Requirement already satisfied: numba>=0.49.0 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from quantecon) (0.59.0)
Requirement already satisfied: numpy>=1.17.0 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from quantecon) (1.26.4)
Requirement already satisfied: requests in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from quantecon) (2.31.0)
Requirement already satisfied: scipy>=1.5.0 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from quantecon) (1.11.4)
Requirement already satisfied: sympy in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from quantecon) (1.12)
Requirement already satisfied: llvmlite<0.43,>=0.42.0dev0 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from numba>=0.49.0->quantecon) (0.42.0)
Requirement already satisfied: charset-normalizer<4,>=2 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from requests->quantecon) (2.0.4)
Requirement already satisfied: idna<4,>=2.5 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from requests->quantecon) (3.4)
Requirement already satisfied: urllib3<3,>=1.21.1 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from requests->quantecon) (2.0.7)
Requirement already satisfied: certifi>=2017.4.17 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from requests->quantecon) (2024.2.2)
Requirement already satisfied: mpmath>=0.19 in c:\users\gyildiri\appdata\local\anaconda3\lib\site-packages (from sympy->quantecon) (1.3.0)
Note: you may need to restart the kernel to use updated packages.
In [83]:
import quantecon as qe

# Assuming trans_matrix_final is your transition matrix as a numpy array
trans_matrix1 = qe.MarkovChain(trans_matrix_final)
In [84]:
import matplotlib.pyplot as plt
import networkx as nx
import numpy as np

# Assuming trans_matrix_final is your numpy array representing the transition matrix
# and row_labels and column_labels are lists of your state names

# Create a directed graph
G = nx.DiGraph()

# Add edges to the graph from the transition matrix
for i, from_state in enumerate(row_labels):
    for j, to_state in enumerate(column_labels):
        probability = trans_matrix_final[i][j]
        if probability > 0:  # Add an edge only if the probability is greater than 0
            G.add_edge(from_state, to_state, weight=probability)

# Draw the graph
plt.figure(figsize=(15, 13))
pos = nx.spring_layout(G)  # Positions for all nodes

# Draw nodes
nx.draw_networkx_nodes(G, pos, node_color="yellow", node_size=700)

# Draw edges
edges = nx.draw_networkx_edges(G, pos, arrowstyle="->", arrowsize=10, edge_cmap=plt.cm.Blues)

# Draw labels
nx.draw_networkx_labels(G, pos, font_size=12)

# Add edge labels (probabilities)
edge_labels = {(u, v): f'{d["weight"]:.2f}' for u, v, d in G.edges(data=True)}
nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels)

plt.title("Markov Graph")
plt.axis("off")  # Turn off the axis
plt.show()
No description has been provided for this image

On the Markov graph above:

  • Nodes 1, 2, 3, and 4 refer to affiliate, mobile, social media and paid search advertising, respectively.

  • Nodes 'conversion' and 'null' refer to conversion and non-conversion, respectively.

  • Node 'start' represents a starting point of all paths, as is requested by the plotting package that we use.

  • Arrows point from the 'departing' node (i.e.,$({n-1})^{th}$ channel) to the 'arriving' node (i.e., ${n}^{th}$ channel).

  • The numeric value along an arrow pointing from node $i$ to node $j$ is the transition probability of a customer first getting exposed to channel $i$ and then to channel $j$.

Looking at the graph, the probability of purchase for a consumer with a simple path of 'start > paid_search > conversion' is $0.39 * 0.04 = 0.0156$, since the transition probability from start to paid search is 0.39, and that from paid search to conversion is 0.04.

Transition heatmap

We may also want to create a heatmap for the transition matrix of our estimated Markov model using the following codes. Note that this map is also created based on first-order markov model results (trans_1st), since graphical representation of the third-order Markov transition heatmap is too densed and difficult to read.

In [86]:
df_plot_trans = trans_1st['transition_matrix']
In [87]:
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd

# Pivot the DataFrame
pivot_df = df_plot_trans.pivot(index='channel_from', columns='channel_to', values='transition_probability')

# Manually reorder the index and columns of pivot_df to match the desired order
ordered_index = ['(start)', '1', '2', '3', '4',]  # Adjust if needed
ordered_columns = ['(start)', '1', '2', '3', '4', '(conversion)', '(null)']  # Adjust if needed

# Reindex the DataFrame according to the specified order
pivot_df = pivot_df.reindex(index=ordered_index, columns=ordered_columns)

# Define the color palette
cols = ["#e7f0fa", "#c9e2f6", "#95cbee", "#0099dc", "#4ab04a", "#ffd73e", "#eec73a", "#e29421"]

# Create the heatmap with annotations
plt.figure(figsize=(8, 6))
sns.heatmap(pivot_df, annot=True, fmt=".2f", cmap=cols, linewidths=.5, cbar_kws={'label': 'Transition Probability'})

# Customize the plot
plt.title('Transition Matrix Heatmap', fontsize=14, fontweight='bold')
plt.xlabel('Channel To', fontsize=12, fontweight='bold')
plt.ylabel('Channel From', fontsize=12, fontweight='bold')
plt.xticks(rotation=45, ha='right')
plt.yticks(rotation=0)

# Show the plot
plt.tight_layout()  # Adjust the layout to fit everything
plt.show()
C:\Users\gyildiri\AppData\Local\anaconda3\Lib\site-packages\seaborn\matrix.py:260: FutureWarning: Format strings passed to MaskedConstant are ignored, but in future may error or produce different behavior
  annotation = ("{:" + self.fmt + "}").format(val)
No description has been provided for this image

In the transition matrix heatmap, the rows represent the 'starting point' and the columns represent the 'ending point' of each one-step transition.From this heatmap, we can reach the same conclusions as with the Markov graph.

Note that there are no rows representing conversion or null states on the matrix because in our setting there are no paths starting from a conversion or starting from a non-conversion. That is also why the cells referring to the transition from start to conversion and null are empty.

Model comparison¶

We would like to see the difference in outcomes of the traditional heuristic approaches (i.e., first-touch, last-touch) and the model-based approach (i.e., Markov chain).

We first use the heuristic_models function to quickly obtain results for the heuristic models:

In [88]:
from ChannelAttribution import heuristic_models
heuristic_models(channel_stack, 'cleaned_path', 'conversion', sep=">")
*** Looking to run more advanced attribution? Try ChannelAttribution Pro for free! Visit https://channelattribution.io/product
Out[88]:
channel_name first_touch last_touch linear_touch
0 affiliate 14.0 18.0 15.162857
1 mobile 17.0 26.0 18.674562
2 social_media 8.0 4.0 14.412425
3 paid_search 28.0 19.0 18.750157

Note that the output of heuristic_function also includes the linear touch model. As explained previously in the chapter, linear (even-split) touch model is a simple heuristic model that assumes that each channel contributes to final conversion equally. We do not focus on this specific model in this chapter as it is not of interest to KNC.

Now we summarize model results of all three models into one table:

In [89]:
import pandas as pd

# Create the summary table DataFrame
table_summary = pd.DataFrame({
    'Item': ["Mobile", "Paid Search", "Affiliate",  "Social Media"],
    'First_Touch': [round(17/67, 3), round(28/67, 3), round(14/67, 3), round(8/67, 3)],
    'Last_Touch': [round(26/67, 3), round(19/67, 3), round(18/67, 3), round(4/67, 3)],
    'Markov_Chain': [0.260, 0.245, 0.238, 0.257]
})
In [90]:
table_summary
Out[90]:
Item First_Touch Last_Touch Markov_Chain
0 Mobile 0.254 0.388 0.260
1 Paid Search 0.418 0.284 0.245
2 Affiliate 0.209 0.269 0.238
3 Social Media 0.119 0.060 0.257

The table above suggests that one can get to very different conclusions by adopting first-touch, last-touch, and Markov chain approach, respectively. Such difference is even clearer if we contrast model results by plotting a bar chart:

In [91]:
import pandas as pd
import matplotlib.pyplot as plt

# Creating the DataFrame
method = ['First Touch']*4 + ['Last Touch']*4 + ['Markov Chain']*4
channel = ['Mobile', 'Paid Search', 'Affiliate', 'Social Media'] * 3
value = [0.254, 0.418, 0.209, 0.119, 0.388, 0.284, 0.269, 0.060, 0.260, 0.245, 0.238, 0.257]
data = pd.DataFrame({'Method': method, 'Channel': channel, 'Value': value})

# Pivot the data for stacked bar chart
pivot_data = data.pivot(index="Method", columns="Channel", values="Value")

# Plotting stacked bar chart
ax = pivot_data.plot(kind='bar', stacked=True)
plt.xlabel("Method")
plt.ylabel("Value")
plt.title("Channel Attribution")

# Place the legend outside the bar chart
plt.legend(title='Channel', bbox_to_anchor=(1.05, 1), loc='upper left')

# Optional: Adjust the subplot params so the plot fits into the figure area.
plt.tight_layout()

plt.show()
No description has been provided for this image

While first-touch model guides KNC to invest more in paid search and less in social media, last-touch model would suggest strong influence of mobile advertising. Finally, Markov graph approach suggests that the contribution made by all four touchpoints are actually quite evenly distributed.

Which model should KNC choose?

There are two options available for KNC when deciding which model to choose:

  1. KNC should try to make sense of these results, first. Prisha can combine the model-based evidence with her managerial intuition. Given her concerns about the touch-based models, Prisha is advised to make her next budget allocation decisions based on Markov chain model output. At the end of the campaign period, she can assess whether there is a significant improvement in conversion rates.

  2. Alternative approach would be to assess the predictive power of attribution models by using a larger dataset. Research on attribution models demonstrate that Markov chain models provide a fairer allocation of weights to channels and perform better than heuristic (e.g., first-touch and last-touch) methods in predicting conversion rates because they take into account the interplay across touchpoints and sequentiality in a customer journey (Anders et al. 2016).In appendix, we illustrate how predictive performance of these models can be assessed using the current dataset. In our illustration, Markov model performs better than heuristic methods. However, we take those results with a grain of salt because some paths to purchase occur only a few times in the test set, which may make conversion probabilities less reliable.

What if a particular touchpoint is removed?

What would have happened to conversion rate when a particular channel was removed from path-to-purchase? To address this question, we will use Shapley value-based approach.

Shapley value-based approach¶

This modelling approach allows us to evaluate the relative importance of a customer touchpoint to conversion.

Using mobile marketing as an example, we will calculate the drop in conversion probability. The scale of the drop will be the importance of mobile marketing in converting customers. Specifically, we will calculate the following:

$$ (No. of conversions/No. of observations) - (No. of conversions_{nomobile}/No. of observations_{nomobile}) $$

We do the same for each of the four touchpoints in the dataset.

The below code chunk searches if each of the paths contains mobile, paid search, affiliate, and social media or not. Then, it generates four indicator variables.

In [92]:
import pandas as pd

# Generating indicator variables
channel_stack['with_mobile'] = channel_stack['cleaned_path'].str.contains('mobile')
channel_stack['with_paid_search'] = channel_stack['cleaned_path'].str.contains('paid_search')
channel_stack['with_affiliate'] = channel_stack['cleaned_path'].str.contains('affiliate')
channel_stack['with_social_media'] = channel_stack['cleaned_path'].str.contains('social_media')

# Calculating conversion and count metrics excluding specific channels
channel_stack['no_mobile_conversion'] = channel_stack.apply(lambda x: x['conversion'] if not x['with_mobile'] else 0, axis=1)
channel_stack['no_mobile_count'] = channel_stack.apply(lambda x: x['conversion'] + x['non_conversion'] if not x['with_mobile'] else 0, axis=1)

channel_stack['no_paidsearch_conversion'] = channel_stack.apply(lambda x: x['conversion'] if not x['with_paid_search'] else 0, axis=1)
channel_stack['no_paidsearch_count'] = channel_stack.apply(lambda x: x['conversion'] + x['non_conversion'] if not x['with_paid_search'] else 0, axis=1)

channel_stack['no_affiliate_conversion'] = channel_stack.apply(lambda x: x['conversion'] if not x['with_affiliate'] else 0, axis=1)
channel_stack['no_affiliate_count'] = channel_stack.apply(lambda x: x['conversion'] + x['non_conversion'] if not x['with_affiliate'] else 0, axis=1)

channel_stack['no_social_media_conversion'] = channel_stack.apply(lambda x: x['conversion'] if not x['with_social_media'] else 0, axis=1)
channel_stack['no_social_media_count'] = channel_stack.apply(lambda x: x['conversion'] + x['non_conversion'] if not x['with_social_media'] else 0, axis=1)

# Displaying the modified DataFrame
channel_stack.head()
Out[92]:
cleaned_path conversion non_conversion with_mobile with_paid_search with_affiliate with_social_media no_mobile_conversion no_mobile_count no_paidsearch_conversion no_paidsearch_count no_affiliate_conversion no_affiliate_count no_social_media_conversion no_social_media_count
0 affiliate>mobile>affiliate>social_media>social_media>mobile>mobile>mobile>mobile 1 0 True False True True 0 0 1 1 0 0 0 0
1 affiliate>paid_search>affiliate>paid_search>affiliate>affiliate>affiliate>affiliate>affiliate>mobile>affiliate>paid_search>paid_search>mobile>paid_search>paid_search>affiliate>paid_search>affiliate>paid_search>affiliate>mobile>affiliate>paid_search>affiliate>mobile>affiliate>affiliate>paid_search>paid_search>paid_search>mobile>affiliate>affiliate>affiliate>affiliate>paid_search>affiliate>paid_search>affiliate>affiliate 1 0 True True True False 0 0 0 0 0 0 1 1
2 affiliate>paid_search>affiliate>paid_search>mobile>mobile>paid_search>paid_search>mobile>affiliate>affiliate>paid_search>mobile>paid_search>affiliate>mobile>paid_search>mobile>mobile>paid_search>affiliate>paid_search>affiliate>social_media>paid_search>mobile>paid_search>affiliate>mobile>affiliate>affiliate>mobile>affiliate>mobile>paid_search>mobile>mobile>mobile>paid_search>affiliate>affiliate>mobile>affiliate>paid_search>mobile 1 0 True True True True 0 0 0 0 0 0 0 0
3 affiliate>paid_search>affiliate>paid_search>social_media>mobile>social_media>paid_search>social_media>affiliate>social_media>paid_search>mobile>paid_search>social_media>mobile>paid_search>mobile>mobile>paid_search>affiliate>paid_search>social_media>social_media>social_media>mobile>paid_search>social_media>mobile>affiliate>affiliate>mobile>affiliate>mobile>paid_search>social_media>mobile>mobile>paid_search>affiliate>social_media>mobile>social_media>social_media>mobile 4 0 True True True True 0 0 0 0 0 0 0 0
4 affiliate>paid_search>affiliate>social_media>social_media>mobile>mobile>paid_search>mobile 2 2 True True True True 0 0 0 0 0 0 0 0

Next, we compute the Shapley values for each customer touchpoint and convert them to percentage terms.

In [93]:
# Calculating the Shapley values for each touchpoint
shapley_mobile = (channel_stack['conversion'].sum() / 105) - (67 - channel_stack['no_mobile_conversion'].sum()) / (105 - channel_stack['no_mobile_count'].sum())
shapley_paidsearch = (channel_stack['conversion'].sum() / 105) - (67 - channel_stack['no_paidsearch_conversion'].sum()) / (105 - channel_stack['no_paidsearch_count'].sum())
shapley_affiliate = (channel_stack['conversion'].sum() / 105) - (67 - channel_stack['no_affiliate_conversion'].sum()) / (105 - channel_stack['no_affiliate_count'].sum())
shapley_socialmedia = (channel_stack['conversion'].sum() / 105) - (67 - channel_stack['no_social_media_conversion'].sum()) / (105 - channel_stack['no_social_media_count'].sum())

shapley_mobile, shapley_paidsearch, shapley_affiliate, shapley_socialmedia
Out[93]:
(0.010644257703081195,
 0.010644257703081195,
 0.0034798534798534675,
 0.008095238095238044)
In [94]:
# Assuming the Shapley values (shapley_mobile, shapley_paidsearch, shapley_affiliate, shapley_socialmedia) are already calculated

# Calculating the Shapley values in percentage terms
total_shapley = shapley_mobile + shapley_paidsearch + shapley_affiliate + shapley_socialmedia

shapley_mobile_pct = shapley_mobile / total_shapley
shapley_paidsearch_pct = shapley_paidsearch / total_shapley
shapley_affiliate_pct = shapley_affiliate / total_shapley
shapley_socialmedia_pct = shapley_socialmedia / total_shapley

shapley_mobile_pct, shapley_paidsearch_pct, shapley_affiliate_pct, shapley_socialmedia_pct
Out[94]:
(0.32389194859690557,
 0.32389194859690557,
 0.10588775242591143,
 0.24632835038027748)

The output from the Shapley model suggests that the impact of mobile and paid search advertising is the largest because they are associated with the largest drop in conversion percentage, followed by social media, and then affiliate. Thus, removing mobile and paid search channels would not be an appropriate action for KNC. Our further advice to Prisha is that she needs to evaluate the strategic implications of a channel removal very carefully with a forward-looking perspective because consumers may be pushed to other touchpoints (e.g. paid search), which may result in a higher cost to KNC. Finally, conclusions from Shapley model are not causal. To understand the causal impact of a channel removal, randomized field experiments should be adopted, in which consumers are randomly assigned to control and treatment groups.

Conclusions¶

Overall, the marketing group at KNC seems to be rather confident of the following decisions to make:

  • KNC should analyze and predict consumer conversion by switching from first- and last-touch heuristic approaches to the Markov chain model.

  • Instead of downgrading their emphasis on affiliate and social media advertising, KNC should keep investing in all of the customer touchpoints.

  • KNC can use the Shapley value-based modelling approach to quantify the potential impact of a channel removal on consumers' purchases. However, the removal of a channel (e.g., social media) may result in significant changes in the customer journey. Consumers may be pushed to other touchpoints (e.g. paid search) that might be more costly to the firm. Therefore, the findings from Shapley model should always be combined with a careful strategic analysis. For instance, even though the model finds zero contribution for the social media channel, the manager may still want to be present on social media to increase the brand awareness in the market.

  • KNC should bear in mind that results obtained from these models do not imply causality. To understand the causal impact of a touchpoint on the relevant performance metrics, KNC should run randomized field experiments.

Challenges with attribution¶

The implementation of attribution models presents a number of challenges as well. For example, it is difficult to account for the fact that each customer may use multiple devices (de Haan et al., 2016). Furthermore, the evolution of data privacy and regulations (e.g. Apple ITP, GDPR) makes it very difficult to track all the touchpoints of a user and obtain representative individual-level data to draw conclusions on the effectiveness of digital advertising channels. Finally, adopting the attribution modelling approach requires a strong cross-functional collaboration (e.g. data input from finance, marketing, and operations) within an organization, which may sometimes be difficult.

Summary¶

In this tutorial, we focused on one of the essential digital marketing tools for marketers: attribution modelling. Attribution models, in general, allow marketers to (i) assess the contribution of different digital touchpoints along the customer’s journey using individual-level data, (ii) tailor future marketing communications to each individual consumer, and (iii) uncover new insights into digital consumer behaviour.

We began thetutorialr with a mini case study on KNC, a firm that sells beauty products. The case study illustrated the importance of attribution modelling from a managerial point of view. Next we demonstrated the application of the attribution models with Python to the case study . There are a few important takeaways from thitutorialer. First, single-touch heuristic attribution models (e.g. first-touch, last-touch) ignore the fact that all the touchpoints on a customer journey may have an impact on the final outcome. Thus, we need more advanced models (e.g. Markov chain) that can capture the dynamics of the customer journey better and produce a fairer allocation of weights to different touchpoints. Second, it is always possible to build a more sophisticated attribution model (e.g. increasing the order for Markovian models) that mimics the customer journey. However, we should always remind ourselves that attribution models are simple illustrations of complex customer journeys. If we increase the complexity of the model, we are likely to lose out on efficiency, which is crucial for large datasets. Third, we can use the Shapley value-based approach to understand the impact of touchpoint removal on the outcome of the customer journey. Nevertheless, we should be aware of the fact that the findings from this method mostly do not have causal meanings. To make causal interpretations, we should use randomized field experiments (Li and Kannan, 2014). However, the decision on conducting experiments sometimes may not be a smooth choice. Removal of a touchpoint (e.g. a banner ad) may result in significant changes in the customer journey. Consumers may be pushed to other touchpoints (e.g. Google’s pay-per-click campaigns) that might be more costly to the firm. Fourth, findings from data-driven models should always be supported by a careful strategic analysis. For instance, even though the model finds zero contribution for a social media ad, the manager may still want to be present on social media to increase brand awareness in the market. Finally, the examples we discussed in thitutorialer use a purchase metric as the outcome variable. Marketers can pick different outcome variables depending on their objectives (e.g. awareness, engagement, web traffic, retention, acquisition . Although attribution models are very useful, embarking on them without due consideration may sometimes do more harm than good. Before diving directly into attribution modelling, we suggest that firms should first determine what their strategic goal is, what they would like to achieve at the end, and relate their data to decision-making.


Appendix¶

Predictive power of attribution models¶

Here we will examine if Markov chain model outperforms the other two heuristics models (i.e., first-touch and last-touch) in terms of predictive accuracy. It is important to mention that we do not include the Shapley value-based in the prediction exercise because the model essentially explores 'what-if' scenarios.

To make comparisons on the predictive power of the aforementioned attribution models, we pull out the last 25 observations from the dataset and use them as a test set, attribution_test.

First-touch model

For the first-touch model, the idea of prediction is quite straightforward. We need extract the first channel exposure of each consumer in the test set and refer back to what we obtained from the training set about conversion probability of each channel type. Depending on what the first touch of each consumer in the test set is, we can simply match the corresponding estimated probability of conversion as predicted outcome.

In [95]:
import pandas as pd

# Creating a DataFrame equivalent to the R data.frame
table_summary2 = pd.DataFrame({
    "Item": ["Mobile", "Paid Search", "Affiliate", "Social Media"],
    "First_Touch": [round(x / 67, 3) for x in [17, 28, 14, 8]]
})

table_summary2
Out[95]:
Item First_Touch
0 Mobile 0.254
1 Paid Search 0.418
2 Affiliate 0.209
3 Social Media 0.119

First-touch model prediction

In [96]:
import pandas as pd

# Get the first channel exposure of each consumer in the test set
attribution_test['first_touch'] = attribution_test['channel1']

# Predict based on training set result
def predict_first_touch(channel):
    if channel == 'mobile':
        return 0.254
    elif channel == 'paid_search':
        return 0.418
    elif channel == 'affiliate':
        return 0.209
    else:  # Assuming all other cases are 'Social Media'
        return 0.119

attribution_test['first_touch_prediction'] = attribution_test['first_touch'].apply(predict_first_touch)

# Compare prediction with actual purchase based on first-touch model
first_touch = pd.DataFrame({
    'user_id': attribution_test['users_id'],
    'first_touch': attribution_test['first_touch'],
    'user_purchase': attribution_test['user_purchase'],
    'predicted_conversion_probability': attribution_test['first_touch_prediction']
})

first_touch
C:\Users\gyildiri\AppData\Local\Temp\ipykernel_29040\1465852174.py:4: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  attribution_test['first_touch'] = attribution_test['channel1']
C:\Users\gyildiri\AppData\Local\Temp\ipykernel_29040\1465852174.py:17: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  attribution_test['first_touch_prediction'] = attribution_test['first_touch'].apply(predict_first_touch)
Out[96]:
user_id first_touch user_purchase predicted_conversion_probability
80 81 mobile 1 0.254
81 82 mobile 1 0.254
82 83 paid_search 1 0.418
83 84 affiliate 1 0.209
84 85 social_media 1 0.119
85 86 affiliate 0 0.209
86 87 paid_search 0 0.418
87 88 paid_search 0 0.418
88 89 mobile 0 0.254
89 90 paid_search 1 0.418
90 91 mobile 0 0.254
91 92 paid_search 0 0.418
92 93 paid_search 1 0.418
93 94 paid_search 1 0.418
94 95 social_media 1 0.119
95 96 mobile 1 0.254
96 97 social_media 1 0.119
97 98 affiliate 1 0.209
98 99 affiliate 0 0.209
99 100 mobile 0 0.254
100 101 affiliate 0 0.209
101 102 paid_search 1 0.418
102 103 paid_search 1 0.418
103 104 social_media 1 0.119
104 105 mobile 1 0.254

Here we spot some issues with predictions from the first-touch model. For example, for consumers 87 and 88, the predicted conversion probabilities are high (0.418 for both consumers). However, when we look at the actual data, we see that they did not purchase at all.

Last-touch model

We follow the same way as we did for the first-touch model above.

In [97]:
import pandas as pd

# Creating a DataFrame
table_summary2 = pd.DataFrame({
    "Item": ["Mobile", "Paid Search", "Affiliate", "Social Media"],
    "Last_Touch": [round(x / 67, 3) for x in [26, 15, 18, 8]]
})

table_summary2
Out[97]:
Item Last_Touch
0 Mobile 0.388
1 Paid Search 0.224
2 Affiliate 0.269
3 Social Media 0.119

Last-touch prediction

In [98]:
import pandas as pd

# Assuming 'attribution_test' is your DataFrame.

# Loop over the rows with index from 80 to 104
for i in range(80, 105):
    # Find the last non-empty channel for the ith row
    # [1:] is used to skip the 'users_id' column and ':-3' to skip the last three columns
    channels = attribution_test.loc[i, 'channel1':'channel45'].replace('', pd.NA).dropna()
    if not channels.empty:
        last_channel = channels.iloc[-1]
    else:
        last_channel = pd.NA  # Or use whatever you deem appropriate for an empty case

    # Assign the last non-empty channel to the 'channel_n' column
    attribution_test.loc[i, 'channel_n'] = last_channel
C:\Users\gyildiri\AppData\Local\Temp\ipykernel_29040\3741261141.py:16: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  attribution_test.loc[i, 'channel_n'] = last_channel
In [53]:
import pandas as pd
attribution_test['channel_n']
# Get the last channel exposure of each consumer in the test set
attribution_test['last_touch'] = attribution_test['channel_n']



# Predict based on training set result
def predict_last_touch(channel):
    if channel == 'mobile':
        return 0.388
    elif channel == 'paid_search':
        return 0.224
    elif channel == 'affiliate':
        return 0.269
    else:  # Assuming all other cases are 'Social Media'
        return 0.119

attribution_test['last_touch_prediction'] = attribution_test['last_touch'].apply(predict_last_touch)

# Compare prediction with actual purchase based on last-touch model
last_touch = pd.DataFrame({
    'user_id': attribution_test['users_id'],
    'last_touch': attribution_test['last_touch'],
    'user_purchase': attribution_test['user_purchase'],
    'predicted_conversion_probability': attribution_test['last_touch_prediction']
})

last_touch
C:\Users\gyildiri\AppData\Local\Temp\ipykernel_29040\3905987594.py:4: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  attribution_test['last_touch'] = attribution_test['channel_n']
C:\Users\gyildiri\AppData\Local\Temp\ipykernel_29040\3905987594.py:19: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  attribution_test['last_touch_prediction'] = attribution_test['last_touch'].apply(predict_last_touch)
Out[53]:
user_id last_touch user_purchase predicted_conversion_probability
80 81 None 1 0.119
81 82 None 1 0.119
82 83 None 1 0.119
83 84 None 1 0.119
84 85 None 1 0.119
85 86 None 0 0.119
86 87 None 0 0.119
87 88 None 0 0.119
88 89 None 0 0.119
89 90 None 1 0.119
90 91 None 0 0.119
91 92 None 0 0.119
92 93 None 1 0.119
93 94 None 1 0.119
94 95 None 1 0.119
95 96 None 1 0.119
96 97 None 1 0.119
97 98 None 1 0.119
98 99 None 0 0.119
99 100 None 0 0.119
100 101 None 0 0.119
101 102 None 1 0.119
102 103 None 1 0.119
103 104 None 1 0.119
104 105 None 1 0.119

From these results, we understand that the last-touch model is able to predict purchases to a certain extent, but definitely not perfectly.

Markov chain model

To get predictions using Markov model, we refer to the third-order Markov transition matrix. Similar to the rationale in first-touch model prediction:

  • We extract all the paths that lead to conversion and retrieve their corresponding transition probabilities to conversion.

  • We then match these paths with the last three channel exposures of each customer in the test set to obtain the predictions.

The code below is to see the number of conversions in the test set.

In [99]:
testing_conversion = sum(attribution_test['user_purchase'])
testing_conversion
Out[99]:
16
In [76]:
attribution_test
Out[76]:
users_id channel1 channel2 channel3 channel4 channel5 channel6 channel7 channel8 channel9 ... channel43 channel44 channel45 user_purchase null_purchase path cleaned_path first_touch first_touch_prediction channel_n
80 81 mobile mobile social_media social_media affiliate affiliate paid_search mobile social_media ... 1 0 mobile>mobile>social_media>social_media>affiliate>affiliate>paid_search>mobile>social_media>affiliate>affiliate>mobile>paid_search>mobile>social_media>social_media>social_media>social_media>affiliate>paid_search>mobile>affiliate>paid_search>mobile>mobile>mobile>paid_search>affiliate>mobile>mobile>paid_search>affiliate>social_media>mobile>mobile>affiliate>paid_search>affiliate>mobile> > > > > > mobile>mobile>social_media>social_media>affiliate>affiliate>paid_search>mobile>social_media>affiliate>affiliate>mobile>paid_search>mobile>social_media>social_media>social_media>social_media>affiliate>paid_search>mobile>affiliate>paid_search>mobile>mobile>mobile>paid_search>affiliate>mobile>mobile>paid_search>affiliate>social_media>mobile>mobile>affiliate>paid_search>affiliate>mobile mobile 0.254
81 82 mobile mobile paid_search mobile paid_search paid_search social_media affiliate paid_search ... 1 0 mobile>mobile>paid_search>mobile>paid_search>paid_search>social_media>affiliate>paid_search>mobile>mobile>social_media>affiliate>social_media> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > mobile>mobile>paid_search>mobile>paid_search>paid_search>social_media>affiliate>paid_search>mobile>mobile>social_media>affiliate>social_media mobile 0.254
82 83 paid_search paid_search paid_search mobile affiliate social_media mobile paid_search social_media ... 1 0 paid_search>paid_search>paid_search>mobile>affiliate>social_media>mobile>paid_search>social_media>mobile>paid_search>mobile>mobile>affiliate>social_media>paid_search>mobile>mobile>affiliate>affiliate>affiliate>mobile>mobile>paid_search>paid_search>paid_search>social_media>mobile>mobile>mobile>mobile> > > > > > > > > > > > > > paid_search>paid_search>paid_search>mobile>affiliate>social_media>mobile>paid_search>social_media>mobile>paid_search>mobile>mobile>affiliate>social_media>paid_search>mobile>mobile>affiliate>affiliate>affiliate>mobile>mobile>paid_search>paid_search>paid_search>social_media>mobile>mobile>mobile>mobile paid_search 0.418
83 84 affiliate paid_search affiliate paid_search social_media mobile social_media paid_search social_media ... social_media paid_search affiliate 1 0 affiliate>paid_search>affiliate>paid_search>social_media>mobile>social_media>paid_search>social_media>affiliate>social_media>paid_search>mobile>paid_search>social_media>mobile>paid_search>mobile>mobile>paid_search>affiliate>paid_search>social_media>social_media>social_media>mobile>paid_search>social_media>mobile>affiliate>affiliate>mobile>affiliate>mobile>paid_search>social_media>mobile>mobile>paid_search>affiliate>social_media>mobile>social_media>paid_search>affiliate affiliate>paid_search>affiliate>paid_search>social_media>mobile>social_media>paid_search>social_media>affiliate>social_media>paid_search>mobile>paid_search>social_media>mobile>paid_search>mobile>mobile>paid_search>affiliate>paid_search>social_media>social_media>social_media>mobile>paid_search>social_media>mobile>affiliate>affiliate>mobile>affiliate>mobile>paid_search>social_media>mobile>mobile>paid_search>affiliate>social_media>mobile>social_media>paid_search>affiliate affiliate 0.209 affiliate
84 85 social_media paid_search social_media paid_search social_media mobile social_media social_media social_media ... 1 0 social_media>paid_search>social_media>paid_search>social_media>mobile>social_media>social_media>social_media>social_media>affiliate>paid_search>paid_search>social_media>paid_search>paid_search>mobile>paid_search>affiliate>paid_search>mobile>social_media>affiliate>social_media>affiliate>social_media>mobile>affiliate>paid_search>paid_search>social_media>social_media>mobile>mobile>affiliate>mobile>paid_search>mobile>social_media>mobile>paid_search> > > > social_media>paid_search>social_media>paid_search>social_media>mobile>social_media>social_media>social_media>social_media>affiliate>paid_search>paid_search>social_media>paid_search>paid_search>mobile>paid_search>affiliate>paid_search>mobile>social_media>affiliate>social_media>affiliate>social_media>mobile>affiliate>paid_search>paid_search>social_media>social_media>mobile>mobile>affiliate>mobile>paid_search>mobile>social_media>mobile>paid_search social_media 0.119
85 86 affiliate paid_search affiliate social_media social_media mobile affiliate affiliate affiliate ... 0 1 affiliate>paid_search>affiliate>social_media>social_media>mobile>affiliate>affiliate>affiliate> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > affiliate>paid_search>affiliate>social_media>social_media>mobile>affiliate>affiliate>affiliate affiliate 0.209
86 87 paid_search affiliate social_media paid_search mobile affiliate paid_search mobile paid_search ... 0 1 paid_search>affiliate>social_media>paid_search>mobile>affiliate>paid_search>mobile>paid_search>social_media>affiliate>mobile>mobile>social_media>mobile>paid_search>affiliate> > > > > > > > > > > > > > > > > > > > > > > > > > > > paid_search>affiliate>social_media>paid_search>mobile>affiliate>paid_search>mobile>paid_search>social_media>affiliate>mobile>mobile>social_media>mobile>paid_search>affiliate paid_search 0.418
87 88 paid_search social_media mobile social_media social_media paid_search ... 0 1 paid_search>social_media>mobile>social_media>social_media>paid_search> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > paid_search>social_media>mobile>social_media>social_media>paid_search paid_search 0.418
88 89 mobile paid_search social_media mobile mobile mobile paid_search affiliate affiliate ... 0 1 mobile>paid_search>social_media>mobile>mobile>mobile>paid_search>affiliate>affiliate>paid_search>mobile>paid_search>paid_search> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > mobile>paid_search>social_media>mobile>mobile>mobile>paid_search>affiliate>affiliate>paid_search>mobile>paid_search>paid_search mobile 0.254
89 90 paid_search social_media affiliate paid_search social_media mobile affiliate paid_search paid_search ... 1 0 paid_search>social_media>affiliate>paid_search>social_media>mobile>affiliate>paid_search>paid_search>mobile> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > paid_search>social_media>affiliate>paid_search>social_media>mobile>affiliate>paid_search>paid_search>mobile paid_search 0.418
90 91 mobile paid_search paid_search paid_search affiliate mobile paid_search paid_search social_media ... 0 1 mobile>paid_search>paid_search>paid_search>affiliate>mobile>paid_search>paid_search>social_media>mobile>social_media>paid_search>social_media>mobile>affiliate>paid_search>paid_search> > > > > > > > > > > > > > > > > > > > > > > > > > > > mobile>paid_search>paid_search>paid_search>affiliate>mobile>paid_search>paid_search>social_media>mobile>social_media>paid_search>social_media>mobile>affiliate>paid_search>paid_search mobile 0.254
91 92 paid_search mobile mobile paid_search affiliate social_media mobile paid_search mobile ... 0 1 paid_search>mobile>mobile>paid_search>affiliate>social_media>mobile>paid_search>mobile>mobile>paid_search>affiliate>social_media>affiliate>affiliate>paid_search>affiliate>affiliate>affiliate>affiliate>affiliate>paid_search>affiliate> > > > > > > > > > > > > > > > > > > > > > paid_search>mobile>mobile>paid_search>affiliate>social_media>mobile>paid_search>mobile>mobile>paid_search>affiliate>social_media>affiliate>affiliate>paid_search>affiliate>affiliate>affiliate>affiliate>affiliate>paid_search>affiliate paid_search 0.418
92 93 paid_search affiliate mobile affiliate mobile paid_search mobile mobile affiliate ... 1 0 paid_search>affiliate>mobile>affiliate>mobile>paid_search>mobile>mobile>affiliate> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > paid_search>affiliate>mobile>affiliate>mobile>paid_search>mobile>mobile>affiliate paid_search 0.418
93 94 paid_search affiliate paid_search social_media affiliate social_media paid_search affiliate ... 1 0 paid_search>affiliate>paid_search>social_media>affiliate>social_media>paid_search>affiliate> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > paid_search>affiliate>paid_search>social_media>affiliate>social_media>paid_search>affiliate paid_search 0.418
94 95 social_media paid_search affiliate social_media mobile paid_search paid_search affiliate mobile ... 1 0 social_media>paid_search>affiliate>social_media>mobile>paid_search>paid_search>affiliate>mobile>affiliate>paid_search>social_media>social_media>affiliate>affiliate>paid_search>social_media>affiliate>paid_search>social_media>mobile>paid_search>affiliate> > > > > > > > > > > > > > > > > > > > > > social_media>paid_search>affiliate>social_media>mobile>paid_search>paid_search>affiliate>mobile>affiliate>paid_search>social_media>social_media>affiliate>affiliate>paid_search>social_media>affiliate>paid_search>social_media>mobile>paid_search>affiliate social_media 0.119
95 96 mobile mobile social_media social_media affiliate affiliate paid_search mobile social_media ... 1 0 mobile>mobile>social_media>social_media>affiliate>affiliate>paid_search>mobile>social_media>affiliate>affiliate>mobile>paid_search>mobile>social_media>social_media>social_media>social_media>affiliate>paid_search>mobile>affiliate>paid_search>mobile>mobile>mobile>paid_search>affiliate>mobile>mobile>paid_search>affiliate>social_media>mobile>mobile>affiliate>social_media>affiliate>paid_search> > > > > > mobile>mobile>social_media>social_media>affiliate>affiliate>paid_search>mobile>social_media>affiliate>affiliate>mobile>paid_search>mobile>social_media>social_media>social_media>social_media>affiliate>paid_search>mobile>affiliate>paid_search>mobile>mobile>mobile>paid_search>affiliate>mobile>mobile>paid_search>affiliate>social_media>mobile>mobile>affiliate>social_media>affiliate>paid_search mobile 0.254
96 97 social_media mobile paid_search mobile paid_search paid_search social_media affiliate paid_search ... 1 0 social_media>mobile>paid_search>mobile>paid_search>paid_search>social_media>affiliate>paid_search>mobile>mobile>social_media>affiliate>social_media> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > social_media>mobile>paid_search>mobile>paid_search>paid_search>social_media>affiliate>paid_search>mobile>mobile>social_media>affiliate>social_media social_media 0.119
97 98 affiliate paid_search paid_search mobile affiliate social_media mobile paid_search social_media ... 1 0 affiliate>paid_search>paid_search>mobile>affiliate>social_media>mobile>paid_search>social_media>mobile>paid_search>mobile>mobile>affiliate>social_media>paid_search>mobile>mobile>affiliate>affiliate>affiliate>mobile>mobile>paid_search>paid_search>paid_search>social_media>mobile>affiliate>mobile>mobile> > > > > > > > > > > > > > affiliate>paid_search>paid_search>mobile>affiliate>social_media>mobile>paid_search>social_media>mobile>paid_search>mobile>mobile>affiliate>social_media>paid_search>mobile>mobile>affiliate>affiliate>affiliate>mobile>mobile>paid_search>paid_search>paid_search>social_media>mobile>affiliate>mobile>mobile affiliate 0.209
98 99 affiliate paid_search affiliate paid_search social_media mobile social_media paid_search social_media ... social_media social_media social_media 0 1 affiliate>paid_search>affiliate>paid_search>social_media>mobile>social_media>paid_search>social_media>affiliate>social_media>paid_search>mobile>paid_search>social_media>mobile>paid_search>mobile>mobile>paid_search>affiliate>paid_search>social_media>social_media>social_media>mobile>paid_search>social_media>mobile>affiliate>affiliate>mobile>affiliate>mobile>paid_search>social_media>mobile>mobile>paid_search>affiliate>social_media>mobile>social_media>social_media>social_media affiliate>paid_search>affiliate>paid_search>social_media>mobile>social_media>paid_search>social_media>affiliate>social_media>paid_search>mobile>paid_search>social_media>mobile>paid_search>mobile>mobile>paid_search>affiliate>paid_search>social_media>social_media>social_media>mobile>paid_search>social_media>mobile>affiliate>affiliate>mobile>affiliate>mobile>paid_search>social_media>mobile>mobile>paid_search>affiliate>social_media>mobile>social_media>social_media>social_media affiliate 0.209 social_media
99 100 mobile paid_search social_media paid_search social_media mobile social_media social_media social_media ... 0 1 mobile>paid_search>social_media>paid_search>social_media>mobile>social_media>social_media>social_media>social_media>affiliate>paid_search>paid_search>social_media>paid_search>paid_search>mobile>paid_search>affiliate>paid_search>mobile>social_media>affiliate>social_media>affiliate>social_media>mobile>affiliate>paid_search>paid_search>social_media>social_media>mobile>mobile>affiliate>mobile>paid_search>mobile>affiliate>mobile>affiliate> > > > mobile>paid_search>social_media>paid_search>social_media>mobile>social_media>social_media>social_media>social_media>affiliate>paid_search>paid_search>social_media>paid_search>paid_search>mobile>paid_search>affiliate>paid_search>mobile>social_media>affiliate>social_media>affiliate>social_media>mobile>affiliate>paid_search>paid_search>social_media>social_media>mobile>mobile>affiliate>mobile>paid_search>mobile>affiliate>mobile>affiliate mobile 0.254
100 101 affiliate paid_search affiliate social_media social_media mobile affiliate paid_search paid_search ... 0 1 affiliate>paid_search>affiliate>social_media>social_media>mobile>affiliate>paid_search>paid_search> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > affiliate>paid_search>affiliate>social_media>social_media>mobile>affiliate>paid_search>paid_search affiliate 0.209
101 102 paid_search paid_search social_media paid_search mobile affiliate paid_search mobile paid_search ... 1 0 paid_search>paid_search>social_media>paid_search>mobile>affiliate>paid_search>mobile>paid_search>social_media>affiliate>mobile>mobile>social_media>mobile>paid_search>paid_search> > > > > > > > > > > > > > > > > > > > > > > > > > > > paid_search>paid_search>social_media>paid_search>mobile>affiliate>paid_search>mobile>paid_search>social_media>affiliate>mobile>mobile>social_media>mobile>paid_search>paid_search paid_search 0.418
102 103 paid_search social_media mobile social_media social_media paid_search ... 1 0 paid_search>social_media>mobile>social_media>social_media>paid_search> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > paid_search>social_media>mobile>social_media>social_media>paid_search paid_search 0.418
103 104 social_media paid_search social_media mobile mobile mobile paid_search affiliate affiliate ... 1 0 social_media>paid_search>social_media>mobile>mobile>mobile>paid_search>affiliate>affiliate>paid_search>social_media>mobile>paid_search> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > social_media>paid_search>social_media>mobile>mobile>mobile>paid_search>affiliate>affiliate>paid_search>social_media>mobile>paid_search social_media 0.119
104 105 mobile mobile paid_search paid_search affiliate mobile paid_search paid_search paid_search ... 1 0 mobile>mobile>paid_search>paid_search>affiliate>mobile>paid_search>paid_search>paid_search>mobile> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > mobile>mobile>paid_search>paid_search>affiliate>mobile>paid_search>paid_search>paid_search>mobile mobile 0.254

25 rows × 53 columns

We understand that in the test set there are 16 conversions out of 25 observations. Next, we move to Markov model predictions.

Markov model prediction

We create a variable last_three_channels in the test set, representing the last three touchpoints that each consumer interacted with along their journey.

In [100]:
import pandas as pd

# Assuming 'attribution_test' DataFrame is already defined with columns representing channels

# Ensuring that there are columns for the last three channels
attribution_test['channel_n'] = None
attribution_test['channel_n_1'] = None
attribution_test['channel_n_2'] = None
attribution_test['last_three_channels'] = None

# Getting the first 25 indices from the DataFrame's index
first_25_indices = attribution_test.index[:25]

# Looping through these indices
for idx in first_25_indices:
    # Filtering out zeros and getting the last three non-zero channels
    non_zero_channels = [channel for channel in attribution_test.loc[idx] if channel != 0]
    num_channels = len(non_zero_channels)

    # Extracting the last three non-zero channels
    channel_n = non_zero_channels[-1] if num_channels >= 1 else 0
    channel_n_1 = non_zero_channels[-2] if num_channels >= 2 else 0
    channel_n_2 = non_zero_channels[-3] if num_channels >= 3 else 0

    # Concatenating the last three channels into a single string
    last_three_channels = '>'.join([str(channel) for channel in [channel_n_2, channel_n_1, channel_n] if channel != 0])

    # Updating the DataFrame
    attribution_test.at[idx, 'channel_n'] = channel_n
    attribution_test.at[idx, 'channel_n_1'] = channel_n_1
    attribution_test.at[idx, 'channel_n_2'] = channel_n_2
    attribution_test.at[idx, 'last_three_channels'] = last_three_channels

# Displaying the modified DataFrame
attribution_test
C:\Users\gyildiri\AppData\Local\Temp\ipykernel_29040\3570705332.py:6: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  attribution_test['channel_n'] = None
C:\Users\gyildiri\AppData\Local\Temp\ipykernel_29040\3570705332.py:7: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  attribution_test['channel_n_1'] = None
C:\Users\gyildiri\AppData\Local\Temp\ipykernel_29040\3570705332.py:8: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  attribution_test['channel_n_2'] = None
C:\Users\gyildiri\AppData\Local\Temp\ipykernel_29040\3570705332.py:9: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  attribution_test['last_three_channels'] = None
Out[100]:
users_id channel1 channel2 channel3 channel4 channel5 channel6 channel7 channel8 channel9 ... user_purchase null_purchase path cleaned_path first_touch first_touch_prediction channel_n channel_n_1 channel_n_2 last_three_channels
80 81 mobile mobile social_media social_media affiliate affiliate paid_search mobile social_media ... 1 0 mobile>mobile>social_media>social_media>affiliate>affiliate>paid_search>mobile>social_media>affiliate>affiliate>mobile>paid_search>mobile>social_media>social_media>social_media>social_media>affiliate>paid_search>mobile>affiliate>paid_search>mobile>mobile>mobile>paid_search>affiliate>mobile>mobile>paid_search>affiliate>social_media>mobile>mobile>affiliate>paid_search>affiliate>mobile> > > > > > mobile>mobile>social_media>social_media>affiliate>affiliate>paid_search>mobile>social_media>affiliate>affiliate>mobile>paid_search>mobile>social_media>social_media>social_media>social_media>affiliate>paid_search>mobile>affiliate>paid_search>mobile>mobile>mobile>paid_search>affiliate>mobile>mobile>paid_search>affiliate>social_media>mobile>mobile>affiliate>paid_search>affiliate>mobile mobile 0.254 None None None None>None>None
81 82 mobile mobile paid_search mobile paid_search paid_search social_media affiliate paid_search ... 1 0 mobile>mobile>paid_search>mobile>paid_search>paid_search>social_media>affiliate>paid_search>mobile>mobile>social_media>affiliate>social_media> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > mobile>mobile>paid_search>mobile>paid_search>paid_search>social_media>affiliate>paid_search>mobile>mobile>social_media>affiliate>social_media mobile 0.254 None None None None>None>None
82 83 paid_search paid_search paid_search mobile affiliate social_media mobile paid_search social_media ... 1 0 paid_search>paid_search>paid_search>mobile>affiliate>social_media>mobile>paid_search>social_media>mobile>paid_search>mobile>mobile>affiliate>social_media>paid_search>mobile>mobile>affiliate>affiliate>affiliate>mobile>mobile>paid_search>paid_search>paid_search>social_media>mobile>mobile>mobile>mobile> > > > > > > > > > > > > > paid_search>paid_search>paid_search>mobile>affiliate>social_media>mobile>paid_search>social_media>mobile>paid_search>mobile>mobile>affiliate>social_media>paid_search>mobile>mobile>affiliate>affiliate>affiliate>mobile>mobile>paid_search>paid_search>paid_search>social_media>mobile>mobile>mobile>mobile paid_search 0.418 None None None None>None>None
83 84 affiliate paid_search affiliate paid_search social_media mobile social_media paid_search social_media ... 1 0 affiliate>paid_search>affiliate>paid_search>social_media>mobile>social_media>paid_search>social_media>affiliate>social_media>paid_search>mobile>paid_search>social_media>mobile>paid_search>mobile>mobile>paid_search>affiliate>paid_search>social_media>social_media>social_media>mobile>paid_search>social_media>mobile>affiliate>affiliate>mobile>affiliate>mobile>paid_search>social_media>mobile>mobile>paid_search>affiliate>social_media>mobile>social_media>paid_search>affiliate affiliate>paid_search>affiliate>paid_search>social_media>mobile>social_media>paid_search>social_media>affiliate>social_media>paid_search>mobile>paid_search>social_media>mobile>paid_search>mobile>mobile>paid_search>affiliate>paid_search>social_media>social_media>social_media>mobile>paid_search>social_media>mobile>affiliate>affiliate>mobile>affiliate>mobile>paid_search>social_media>mobile>mobile>paid_search>affiliate>social_media>mobile>social_media>paid_search>affiliate affiliate 0.209 None None None None>None>None
84 85 social_media paid_search social_media paid_search social_media mobile social_media social_media social_media ... 1 0 social_media>paid_search>social_media>paid_search>social_media>mobile>social_media>social_media>social_media>social_media>affiliate>paid_search>paid_search>social_media>paid_search>paid_search>mobile>paid_search>affiliate>paid_search>mobile>social_media>affiliate>social_media>affiliate>social_media>mobile>affiliate>paid_search>paid_search>social_media>social_media>mobile>mobile>affiliate>mobile>paid_search>mobile>social_media>mobile>paid_search> > > > social_media>paid_search>social_media>paid_search>social_media>mobile>social_media>social_media>social_media>social_media>affiliate>paid_search>paid_search>social_media>paid_search>paid_search>mobile>paid_search>affiliate>paid_search>mobile>social_media>affiliate>social_media>affiliate>social_media>mobile>affiliate>paid_search>paid_search>social_media>social_media>mobile>mobile>affiliate>mobile>paid_search>mobile>social_media>mobile>paid_search social_media 0.119 None None None None>None>None
85 86 affiliate paid_search affiliate social_media social_media mobile affiliate affiliate affiliate ... 0 1 affiliate>paid_search>affiliate>social_media>social_media>mobile>affiliate>affiliate>affiliate> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > affiliate>paid_search>affiliate>social_media>social_media>mobile>affiliate>affiliate>affiliate affiliate 0.209 None None None None>None>None
86 87 paid_search affiliate social_media paid_search mobile affiliate paid_search mobile paid_search ... 0 1 paid_search>affiliate>social_media>paid_search>mobile>affiliate>paid_search>mobile>paid_search>social_media>affiliate>mobile>mobile>social_media>mobile>paid_search>affiliate> > > > > > > > > > > > > > > > > > > > > > > > > > > > paid_search>affiliate>social_media>paid_search>mobile>affiliate>paid_search>mobile>paid_search>social_media>affiliate>mobile>mobile>social_media>mobile>paid_search>affiliate paid_search 0.418 None None None None>None>None
87 88 paid_search social_media mobile social_media social_media paid_search ... 0 1 paid_search>social_media>mobile>social_media>social_media>paid_search> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > paid_search>social_media>mobile>social_media>social_media>paid_search paid_search 0.418 None None None None>None>None
88 89 mobile paid_search social_media mobile mobile mobile paid_search affiliate affiliate ... 0 1 mobile>paid_search>social_media>mobile>mobile>mobile>paid_search>affiliate>affiliate>paid_search>mobile>paid_search>paid_search> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > mobile>paid_search>social_media>mobile>mobile>mobile>paid_search>affiliate>affiliate>paid_search>mobile>paid_search>paid_search mobile 0.254 None None None None>None>None
89 90 paid_search social_media affiliate paid_search social_media mobile affiliate paid_search paid_search ... 1 0 paid_search>social_media>affiliate>paid_search>social_media>mobile>affiliate>paid_search>paid_search>mobile> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > paid_search>social_media>affiliate>paid_search>social_media>mobile>affiliate>paid_search>paid_search>mobile paid_search 0.418 None None None None>None>None
90 91 mobile paid_search paid_search paid_search affiliate mobile paid_search paid_search social_media ... 0 1 mobile>paid_search>paid_search>paid_search>affiliate>mobile>paid_search>paid_search>social_media>mobile>social_media>paid_search>social_media>mobile>affiliate>paid_search>paid_search> > > > > > > > > > > > > > > > > > > > > > > > > > > > mobile>paid_search>paid_search>paid_search>affiliate>mobile>paid_search>paid_search>social_media>mobile>social_media>paid_search>social_media>mobile>affiliate>paid_search>paid_search mobile 0.254 None None None None>None>None
91 92 paid_search mobile mobile paid_search affiliate social_media mobile paid_search mobile ... 0 1 paid_search>mobile>mobile>paid_search>affiliate>social_media>mobile>paid_search>mobile>mobile>paid_search>affiliate>social_media>affiliate>affiliate>paid_search>affiliate>affiliate>affiliate>affiliate>affiliate>paid_search>affiliate> > > > > > > > > > > > > > > > > > > > > > paid_search>mobile>mobile>paid_search>affiliate>social_media>mobile>paid_search>mobile>mobile>paid_search>affiliate>social_media>affiliate>affiliate>paid_search>affiliate>affiliate>affiliate>affiliate>affiliate>paid_search>affiliate paid_search 0.418 None None None None>None>None
92 93 paid_search affiliate mobile affiliate mobile paid_search mobile mobile affiliate ... 1 0 paid_search>affiliate>mobile>affiliate>mobile>paid_search>mobile>mobile>affiliate> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > paid_search>affiliate>mobile>affiliate>mobile>paid_search>mobile>mobile>affiliate paid_search 0.418 None None None None>None>None
93 94 paid_search affiliate paid_search social_media affiliate social_media paid_search affiliate ... 1 0 paid_search>affiliate>paid_search>social_media>affiliate>social_media>paid_search>affiliate> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > paid_search>affiliate>paid_search>social_media>affiliate>social_media>paid_search>affiliate paid_search 0.418 None None None None>None>None
94 95 social_media paid_search affiliate social_media mobile paid_search paid_search affiliate mobile ... 1 0 social_media>paid_search>affiliate>social_media>mobile>paid_search>paid_search>affiliate>mobile>affiliate>paid_search>social_media>social_media>affiliate>affiliate>paid_search>social_media>affiliate>paid_search>social_media>mobile>paid_search>affiliate> > > > > > > > > > > > > > > > > > > > > > social_media>paid_search>affiliate>social_media>mobile>paid_search>paid_search>affiliate>mobile>affiliate>paid_search>social_media>social_media>affiliate>affiliate>paid_search>social_media>affiliate>paid_search>social_media>mobile>paid_search>affiliate social_media 0.119 None None None None>None>None
95 96 mobile mobile social_media social_media affiliate affiliate paid_search mobile social_media ... 1 0 mobile>mobile>social_media>social_media>affiliate>affiliate>paid_search>mobile>social_media>affiliate>affiliate>mobile>paid_search>mobile>social_media>social_media>social_media>social_media>affiliate>paid_search>mobile>affiliate>paid_search>mobile>mobile>mobile>paid_search>affiliate>mobile>mobile>paid_search>affiliate>social_media>mobile>mobile>affiliate>social_media>affiliate>paid_search> > > > > > mobile>mobile>social_media>social_media>affiliate>affiliate>paid_search>mobile>social_media>affiliate>affiliate>mobile>paid_search>mobile>social_media>social_media>social_media>social_media>affiliate>paid_search>mobile>affiliate>paid_search>mobile>mobile>mobile>paid_search>affiliate>mobile>mobile>paid_search>affiliate>social_media>mobile>mobile>affiliate>social_media>affiliate>paid_search mobile 0.254 None None None None>None>None
96 97 social_media mobile paid_search mobile paid_search paid_search social_media affiliate paid_search ... 1 0 social_media>mobile>paid_search>mobile>paid_search>paid_search>social_media>affiliate>paid_search>mobile>mobile>social_media>affiliate>social_media> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > social_media>mobile>paid_search>mobile>paid_search>paid_search>social_media>affiliate>paid_search>mobile>mobile>social_media>affiliate>social_media social_media 0.119 None None None None>None>None
97 98 affiliate paid_search paid_search mobile affiliate social_media mobile paid_search social_media ... 1 0 affiliate>paid_search>paid_search>mobile>affiliate>social_media>mobile>paid_search>social_media>mobile>paid_search>mobile>mobile>affiliate>social_media>paid_search>mobile>mobile>affiliate>affiliate>affiliate>mobile>mobile>paid_search>paid_search>paid_search>social_media>mobile>affiliate>mobile>mobile> > > > > > > > > > > > > > affiliate>paid_search>paid_search>mobile>affiliate>social_media>mobile>paid_search>social_media>mobile>paid_search>mobile>mobile>affiliate>social_media>paid_search>mobile>mobile>affiliate>affiliate>affiliate>mobile>mobile>paid_search>paid_search>paid_search>social_media>mobile>affiliate>mobile>mobile affiliate 0.209 None None None None>None>None
98 99 affiliate paid_search affiliate paid_search social_media mobile social_media paid_search social_media ... 0 1 affiliate>paid_search>affiliate>paid_search>social_media>mobile>social_media>paid_search>social_media>affiliate>social_media>paid_search>mobile>paid_search>social_media>mobile>paid_search>mobile>mobile>paid_search>affiliate>paid_search>social_media>social_media>social_media>mobile>paid_search>social_media>mobile>affiliate>affiliate>mobile>affiliate>mobile>paid_search>social_media>mobile>mobile>paid_search>affiliate>social_media>mobile>social_media>social_media>social_media affiliate>paid_search>affiliate>paid_search>social_media>mobile>social_media>paid_search>social_media>affiliate>social_media>paid_search>mobile>paid_search>social_media>mobile>paid_search>mobile>mobile>paid_search>affiliate>paid_search>social_media>social_media>social_media>mobile>paid_search>social_media>mobile>affiliate>affiliate>mobile>affiliate>mobile>paid_search>social_media>mobile>mobile>paid_search>affiliate>social_media>mobile>social_media>social_media>social_media affiliate 0.209 None None None None>None>None
99 100 mobile paid_search social_media paid_search social_media mobile social_media social_media social_media ... 0 1 mobile>paid_search>social_media>paid_search>social_media>mobile>social_media>social_media>social_media>social_media>affiliate>paid_search>paid_search>social_media>paid_search>paid_search>mobile>paid_search>affiliate>paid_search>mobile>social_media>affiliate>social_media>affiliate>social_media>mobile>affiliate>paid_search>paid_search>social_media>social_media>mobile>mobile>affiliate>mobile>paid_search>mobile>affiliate>mobile>affiliate> > > > mobile>paid_search>social_media>paid_search>social_media>mobile>social_media>social_media>social_media>social_media>affiliate>paid_search>paid_search>social_media>paid_search>paid_search>mobile>paid_search>affiliate>paid_search>mobile>social_media>affiliate>social_media>affiliate>social_media>mobile>affiliate>paid_search>paid_search>social_media>social_media>mobile>mobile>affiliate>mobile>paid_search>mobile>affiliate>mobile>affiliate mobile 0.254 None None None None>None>None
100 101 affiliate paid_search affiliate social_media social_media mobile affiliate paid_search paid_search ... 0 1 affiliate>paid_search>affiliate>social_media>social_media>mobile>affiliate>paid_search>paid_search> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > affiliate>paid_search>affiliate>social_media>social_media>mobile>affiliate>paid_search>paid_search affiliate 0.209 None None None None>None>None
101 102 paid_search paid_search social_media paid_search mobile affiliate paid_search mobile paid_search ... 1 0 paid_search>paid_search>social_media>paid_search>mobile>affiliate>paid_search>mobile>paid_search>social_media>affiliate>mobile>mobile>social_media>mobile>paid_search>paid_search> > > > > > > > > > > > > > > > > > > > > > > > > > > > paid_search>paid_search>social_media>paid_search>mobile>affiliate>paid_search>mobile>paid_search>social_media>affiliate>mobile>mobile>social_media>mobile>paid_search>paid_search paid_search 0.418 None None None None>None>None
102 103 paid_search social_media mobile social_media social_media paid_search ... 1 0 paid_search>social_media>mobile>social_media>social_media>paid_search> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > paid_search>social_media>mobile>social_media>social_media>paid_search paid_search 0.418 None None None None>None>None
103 104 social_media paid_search social_media mobile mobile mobile paid_search affiliate affiliate ... 1 0 social_media>paid_search>social_media>mobile>mobile>mobile>paid_search>affiliate>affiliate>paid_search>social_media>mobile>paid_search> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > social_media>paid_search>social_media>mobile>mobile>mobile>paid_search>affiliate>affiliate>paid_search>social_media>mobile>paid_search social_media 0.119 None None None None>None>None
104 105 mobile mobile paid_search paid_search affiliate mobile paid_search paid_search paid_search ... 1 0 mobile>mobile>paid_search>paid_search>affiliate>mobile>paid_search>paid_search>paid_search>mobile> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > mobile>mobile>paid_search>paid_search>affiliate>mobile>paid_search>paid_search>paid_search>mobile mobile 0.254 None None None None>None>None

25 rows × 56 columns

In [47]:
 

Now we simply need to match the last three exposures of consumers in the test set with their corresponding conversion probability, if any.

In [101]:
attribution_test.shape
Out[101]:
(25, 56)
In [102]:
# Merging 'attribution_test' with 'markov_result'
merged_attribution = pd.merge(attribution_test, markov_result, left_on="last_three_channels", right_on="channel_3rd_order", how="left")

merged_attribution.shape  # Displaying the first few rows of the merged DataFrame
Out[102]:
(25, 58)
In [50]:
merged_attribution
Out[50]:
users_id channel1 channel2 channel3 channel4 channel5 channel6 channel7 channel8 channel9 ... first_touch first_touch_prediction channel_n last_touch last_touch_prediction channel_n_1 channel_n_2 last_three_channels channel_3rd_order transition_probability
0 81 mobile mobile social_media social_media affiliate affiliate paid_search mobile social_media ... mobile 0.254 None 0.119 None None None>None>None NaN NaN
1 82 mobile mobile paid_search mobile paid_search paid_search social_media affiliate paid_search ... mobile 0.254 None 0.119 None None None>None>None NaN NaN
2 83 paid_search paid_search paid_search mobile affiliate social_media mobile paid_search social_media ... paid_search 0.418 None 0.119 None None None>None>None NaN NaN
3 84 affiliate paid_search affiliate paid_search social_media mobile social_media paid_search social_media ... affiliate 0.209 None affiliate 0.269 None None None>None>None NaN NaN
4 85 social_media paid_search social_media paid_search social_media mobile social_media social_media social_media ... social_media 0.119 None 0.119 None None None>None>None NaN NaN
5 86 affiliate paid_search affiliate social_media social_media mobile affiliate affiliate affiliate ... affiliate 0.209 None 0.119 None None None>None>None NaN NaN
6 87 paid_search affiliate social_media paid_search mobile affiliate paid_search mobile paid_search ... paid_search 0.418 None 0.119 None None None>None>None NaN NaN
7 88 paid_search social_media mobile social_media social_media paid_search ... paid_search 0.418 None 0.119 None None None>None>None NaN NaN
8 89 mobile paid_search social_media mobile mobile mobile paid_search affiliate affiliate ... mobile 0.254 None 0.119 None None None>None>None NaN NaN
9 90 paid_search social_media affiliate paid_search social_media mobile affiliate paid_search paid_search ... paid_search 0.418 None 0.119 None None None>None>None NaN NaN
10 91 mobile paid_search paid_search paid_search affiliate mobile paid_search paid_search social_media ... mobile 0.254 None 0.119 None None None>None>None NaN NaN
11 92 paid_search mobile mobile paid_search affiliate social_media mobile paid_search mobile ... paid_search 0.418 None 0.119 None None None>None>None NaN NaN
12 93 paid_search affiliate mobile affiliate mobile paid_search mobile mobile affiliate ... paid_search 0.418 None 0.119 None None None>None>None NaN NaN
13 94 paid_search affiliate paid_search social_media affiliate social_media paid_search affiliate ... paid_search 0.418 None 0.119 None None None>None>None NaN NaN
14 95 social_media paid_search affiliate social_media mobile paid_search paid_search affiliate mobile ... social_media 0.119 None 0.119 None None None>None>None NaN NaN
15 96 mobile mobile social_media social_media affiliate affiliate paid_search mobile social_media ... mobile 0.254 None 0.119 None None None>None>None NaN NaN
16 97 social_media mobile paid_search mobile paid_search paid_search social_media affiliate paid_search ... social_media 0.119 None 0.119 None None None>None>None NaN NaN
17 98 affiliate paid_search paid_search mobile affiliate social_media mobile paid_search social_media ... affiliate 0.209 None 0.119 None None None>None>None NaN NaN
18 99 affiliate paid_search affiliate paid_search social_media mobile social_media paid_search social_media ... affiliate 0.209 None social_media 0.119 None None None>None>None NaN NaN
19 100 mobile paid_search social_media paid_search social_media mobile social_media social_media social_media ... mobile 0.254 None 0.119 None None None>None>None NaN NaN
20 101 affiliate paid_search affiliate social_media social_media mobile affiliate paid_search paid_search ... affiliate 0.209 None 0.119 None None None>None>None NaN NaN
21 102 paid_search paid_search social_media paid_search mobile affiliate paid_search mobile paid_search ... paid_search 0.418 None 0.119 None None None>None>None NaN NaN
22 103 paid_search social_media mobile social_media social_media paid_search ... paid_search 0.418 None 0.119 None None None>None>None NaN NaN
23 104 social_media paid_search social_media mobile mobile mobile paid_search affiliate affiliate ... social_media 0.119 None 0.119 None None None>None>None NaN NaN
24 105 mobile mobile paid_search paid_search affiliate mobile paid_search paid_search paid_search ... mobile 0.254 None 0.119 None None None>None>None NaN NaN

25 rows × 60 columns

After matching, we can summarize the results on actual vs. predicted values by listing customers in the test set together with their actual conversion (user_purchase) and predicted conversion probability (predicted_conversion_probability), and order the new dataset by predicted probability in descending order.

In [103]:
import pandas as pd

# Assuming 'merged_attribution' DataFrame is already defined in your environment

# Creating a new DataFrame 'performance'
performance = pd.DataFrame({
    'users_id': merged_attribution['users_id'],
    'user_purchase': merged_attribution['user_purchase'],
    'predicted_conversion_probability': merged_attribution['transition_probability']
})

# Replacing NA/NaN values with 0
performance.fillna(0, inplace=True)

# Sorting 'performance' by 'predicted_conversion_probability' in descending order
markov_prediction = performance
#.sort_values(by='predicted_conversion_probability', ascending=False)

# Displaying the first few rows of the DataFrame
markov_prediction
Out[103]:
users_id user_purchase predicted_conversion_probability
0 81 1 0.0
1 82 1 0.0
2 83 1 0.0
3 84 1 0.0
4 85 1 0.0
5 86 0 0.0
6 87 0 0.0
7 88 0 0.0
8 89 0 0.0
9 90 1 0.0
10 91 0 0.0
11 92 0 0.0
12 93 1 0.0
13 94 1 0.0
14 95 1 0.0
15 96 1 0.0
16 97 1 0.0
17 98 1 0.0
18 99 0 0.0
19 100 0 0.0
20 101 0 0.0
21 102 1 0.0
22 103 1 0.0
23 104 1 0.0
24 105 1 0.0

Predictive performance comparison across models¶

To determine which model performs best in predicting consumer conversion, we compare predicted conversion probabilities for each consumer with the actual conversion data. Specifically, we need to compare model performance in terms of accuracy and efficiency in identifying consumers with a high likelihood of conversion.

To do this, we need to take the following steps:

  • Rank the data by predicted conversion probability (i.e., predicted_conversion_probability) in descending order. Note that an observation with non-conversion (i.e., user_purchase=0) might correspond to a positive of even high predicted probability of conversion. This tends to happen if our prediction model performs poorly.

  • Starting from the first observation of ranked data, count the cumulative number of actual conversions. The 'quicker' the cumulative count reaches the total number of conversions (16 in this case), the more capable the model is in identifying promising consumers through their channel exposure paths.

Let us start with the first-touch model.

First-touch model

In [104]:
# Sorting 'first_touch' DataFrame by 'predicted_conversion_probability' in descending order
first_touch_prediction = first_touch.sort_values(by='predicted_conversion_probability', ascending=False)

# Displaying the first few rows of the DataFrame
first_touch_prediction
Out[104]:
user_id first_touch user_purchase predicted_conversion_probability
92 93 paid_search 1 0.418
82 83 paid_search 1 0.418
102 103 paid_search 1 0.418
101 102 paid_search 1 0.418
86 87 paid_search 0 0.418
87 88 paid_search 0 0.418
89 90 paid_search 1 0.418
93 94 paid_search 1 0.418
91 92 paid_search 0 0.418
81 82 mobile 1 0.254
99 100 mobile 0 0.254
95 96 mobile 1 0.254
80 81 mobile 1 0.254
90 91 mobile 0 0.254
88 89 mobile 0 0.254
104 105 mobile 1 0.254
97 98 affiliate 1 0.209
98 99 affiliate 0 0.209
85 86 affiliate 0 0.209
100 101 affiliate 0 0.209
83 84 affiliate 1 0.209
94 95 social_media 1 0.119
96 97 social_media 1 0.119
84 85 social_media 1 0.119
103 104 social_media 1 0.119

By taking a quick look, we immediately notice that the fifth and sixth consumers in the above table did not make purchase (i.e., user_purchase=0). However, the model predicts conversion probabilities 41.8% for both of them.

Knowing that the first channel exposure for both consumers is paid search, we can explain the discrepancy between actual and predicted data in the following way. The model estimation using the training set gives us high transition probability from paid search to conversion whereas in the test set consumers do not behave homogeneously with those in the training set.

Now let's count the cumulative number of actual conversions based on the ordered data.

In [105]:
# Adding a column for cumulative conversion using the 'cumsum' function
first_touch_prediction['cumu_conversion_ft'] = first_touch_prediction['user_purchase'].cumsum()

# Displaying the first five rows of the DataFrame
first_touch_prediction
Out[105]:
user_id first_touch user_purchase predicted_conversion_probability cumu_conversion_ft
92 93 paid_search 1 0.418 1
82 83 paid_search 1 0.418 2
102 103 paid_search 1 0.418 3
101 102 paid_search 1 0.418 4
86 87 paid_search 0 0.418 4
87 88 paid_search 0 0.418 4
89 90 paid_search 1 0.418 5
93 94 paid_search 1 0.418 6
91 92 paid_search 0 0.418 6
81 82 mobile 1 0.254 7
99 100 mobile 0 0.254 7
95 96 mobile 1 0.254 8
80 81 mobile 1 0.254 9
90 91 mobile 0 0.254 9
88 89 mobile 0 0.254 9
104 105 mobile 1 0.254 10
97 98 affiliate 1 0.209 11
98 99 affiliate 0 0.209 11
85 86 affiliate 0 0.209 11
100 101 affiliate 0 0.209 11
83 84 affiliate 1 0.209 12
94 95 social_media 1 0.119 13
96 97 social_media 1 0.119 14
84 85 social_media 1 0.119 15
103 104 social_media 1 0.119 16

We can see that among the first nine customers with highest predicted conversion probabilities, there are six customers (users id: 93, 83, 103, 102, 90, 94) who actually made purchases. This means that using the top 20% valuable customers (in terms of conversion potential) based on our prediction, we can capture only six out of 16 total conversions in the test set.


Question: Do you think this is a satisfactory performance? Why or why not? (Hint: think of what the output would look like if we made predictions via a randomly selected data.)


Last-touch model

We follow the same steps as we did above with the first-touch model.

In [106]:
# Sorting 'last_touch' DataFrame by 'predicted_conversion_probability' in descending order
last_touch_prediction = last_touch.sort_values(by='predicted_conversion_probability', ascending=False)

# Adding a column for cumulative conversion using the 'cumsum' function
last_touch_prediction['cumu_conversion_lt'] = last_touch_prediction['user_purchase'].cumsum()

# Displaying the first five rows of the DataFrame
last_touch_prediction
Out[106]:
user_id last_touch user_purchase predicted_conversion_probability cumu_conversion_lt
80 81 None 1 0.119 1
93 94 None 1 0.119 2
103 104 None 1 0.119 3
102 103 None 1 0.119 4
101 102 None 1 0.119 5
100 101 None 0 0.119 5
99 100 None 0 0.119 5
98 99 None 0 0.119 5
97 98 None 1 0.119 6
96 97 None 1 0.119 7
95 96 None 1 0.119 8
94 95 None 1 0.119 9
92 93 None 1 0.119 10
81 82 None 1 0.119 11
91 92 None 0 0.119 11
90 91 None 0 0.119 11
89 90 None 1 0.119 12
88 89 None 0 0.119 12
87 88 None 0 0.119 12
86 87 None 0 0.119 12
85 86 None 0 0.119 12
84 85 None 1 0.119 13
83 84 None 1 0.119 14
82 83 None 1 0.119 15
104 105 None 1 0.119 16

Next, we look at the Markov model predictions.

Markov model

As with first- and last-touch models, we need to calculate the cumulative actual conversions that Markov model is able to capture.

In [107]:
# Adding a column for cumulative conversion using the 'cumsum' function
markov_prediction['cumu_conversion_mm'] = markov_prediction['user_purchase'].cumsum()

# Displaying the first five rows of the DataFrame
markov_prediction.head(6)
Out[107]:
users_id user_purchase predicted_conversion_probability cumu_conversion_mm
0 81 1 0.0 1
1 82 1 0.0 2
2 83 1 0.0 3
3 84 1 0.0 4
4 85 1 0.0 5
5 86 0 0.0 5

From this output, it seems that Markov chain performs better than the first-touch model. For the first five customers with highest predicted conversion probabilities, all of them have actually made purchases in the data.


Question: Can you comment on the predictive performance of the Markov model vs. first-touch model?


Model performance visualization¶

Finally, it is always a good idea to further visualize the results on models' predictive power so that Levi and Emma can make more informed decisions.

As described above, we can plot the cumulative actual conversions captured by each model onto one graph.

In the following code, Cumu_Markov and Cumu_First_Touch refer to cumulative number of actual conversions captured by Markov and first-touch predictions, respectively. Observation identifies the observation with $n^{th}$ (n=1,2,..., 25) highest predicted conversion probability.

In [108]:
# Resetting indices of the source DataFrames
markov_reset = markov_prediction.reset_index(drop=True)
first_touch_reset = first_touch_prediction.reset_index(drop=True)
last_touch_reset = last_touch_prediction.reset_index(drop=True)

# Preparing the dataset for plotting
plot_raw = pd.DataFrame({
    'Observation': range(1, 26),
    'Cumu_Markov': markov_reset['cumu_conversion_mm'],
    'Cumu_First_Touch': first_touch_reset['cumu_conversion_ft'],
    'Cumu_Last_Touch': last_touch_reset['cumu_conversion_lt']
})

plot_raw
Out[108]:
Observation Cumu_Markov Cumu_First_Touch Cumu_Last_Touch
0 1 1 1 1
1 2 2 2 2
2 3 3 3 3
3 4 4 4 4
4 5 5 4 5
5 6 5 4 5
6 7 5 5 5
7 8 5 6 5
8 9 5 6 6
9 10 6 7 7
10 11 6 7 8
11 12 6 8 9
12 13 7 9 10
13 14 8 9 11
14 15 9 9 11
15 16 10 10 11
16 17 11 11 12
17 18 12 11 12
18 19 12 11 12
19 20 12 11 12
20 21 12 12 12
21 22 13 13 13
22 23 14 14 14
23 24 15 15 15
24 25 16 16 16
In [109]:
import pandas as pd
import matplotlib.pyplot as plt

# Assuming 'plot_raw' DataFrame is already defined in your environment

# Extracting and normalizing data for plotting
plot_markov = [plot_raw['Cumu_Markov'][i - 1] / 16 for i in [5, 10, 15, 20, 25]]
plot_first_touch = [plot_raw['Cumu_First_Touch'][i - 1] / 16 for i in [5, 10, 15, 20, 25]]
plot_last_touch = [plot_raw['Cumu_Last_Touch'][i - 1] / 16 for i in [5, 10, 15, 20, 25]]

# Creating a new DataFrame for plotting
plot = pd.DataFrame({
    'Markov': plot_markov,
    'First_Touch': plot_first_touch,
    'Last_Touch': plot_last_touch,
    'Observation': [i / 25 for i in [5, 10, 15, 20, 25]]
})

# Plotting using matplotlib
plt.figure(figsize=(10, 6))
plt.plot('Observation', 'Markov', data=plot, color='darkred', label='Markov')
plt.plot('Observation', 'First_Touch', data=plot, color='blue', label='First Touch')
plt.plot('Observation', 'Last_Touch', data=plot, color='orange', label='Last Touch')

plt.xlabel('% of Observations')
plt.ylabel('Cumulative % of Actual Conversion Captured')
plt.title('Model performance comparison')
plt.legend()

plt.show()
No description has been provided for this image

On the above graph,

  • The dark red line represents Markov model while the blue line refers to the first-touch model, and the orange line refers to the last-touch model.

  • The horizontal axis refers to the percentage of ranked observations. We have 25 predicted observations in total. The first 20% means the first five observations with highest predicted conversion probability.

  • The vertical axis refers to the percentage of actual conversions captured by our model using a specific % of ranked observations. For example, a $Y$% capture with $X$% observations means that our model is able to capture $Y$% of actual sales by using the first $X$% of ranked observations.

The graph above further shows that the last-touch model’s performance is slightly better than that of Markov chain model. One can argue that Markov chain model performs relatively better but its predictive accuracy is not high. Tweaking the Markov model further (e.g. trying higher-order models) with a more balanced and larger dataset, we can better understand how much can be gained from this model in practice.